Java SE 是什么,包括哪些内容(十六)?

本文内容参考自Java8标准

如果你有缘点开此博文,请至少看到后面的知识点分类列表!你就能知道Java的访问权限控制到底包括了哪些内容。

个人认为编程语言中之所以需要访问权限控制是因为类的创建者不可能一次性就写出最完美的代码,一旦客户端程序员使用了类创建者提供的代码,但是在后期,类创建者又发现了更好的实现,他必定需要对类代码进行修改,那么,需要满足两个限制条件:
1、类创建者对类代码的任何修改不能对客户端程序员的代码产生任何影响!
2、客户端程序员的代码不需要做任何修改就能享受到这些修改带来的好处!

所以,一句话总结就是:类创建者需要将客户端程序员不能使用的部分进行隐藏,而仅仅暴露允许他们使用的部分。
以下内容摘自Java编程思想第四版
访问权限控制(或隐藏具体实现)与"最初的实现并不恰当"有关!
所有优秀的作者,包括那些编写软件的程序员,都清楚他们一开始写的代码,必须要经过不断的思考以及修改,才会变得完美,比如你在实现了一个功能以后,过几天,再回过头来审视这段代码,有可能你会找到更好的方式去实现相同的功能,这正是重构的原动力之一,重构即重写代码,使得它更易读,更易理解,并因此而更具有可维护性(最明显的一点是:在最开始的时候,你的代码中可能会有大量的变量声明,可是过一段时间以后,你会发现其中很大一部分是不需要的。因为并不是每次你需要变量的时候都需要去重新声明一个,其实你可以看看之前已经声明了的变量是否已经结束了它的上一个使用范围。如果是,你可以接着让它进入下一个范围,所以,这种变量的连续使用可能会导致你"看不懂"别人的代码)。
但是,在这种修改和完善代码的愿望之下, 也存在着巨大的压力,通常总是会有一些消费者(客户端程序员)需要你的代码在某一些方面保持不变,因此你想改变代码,而他们却想让代码保持不变,由此而产生了在面向对象设计中需要考虑的一个基本问题:如何把变动的事物和不变的事物区分开来。
这对类库而言尤为重要,该类库的消费者所需要实现的功能必须依赖它所使用的那部分类库,并且,如果类库有了新的版本,他们并不需要改写代码,但是从另一个方面来说,类库的开发者必须有权限进行修改和改进,并确保客户端代码并不会因为这些改动而受到影响。
这些目标可以用过约定来达到,例如:类库开发者必须同意在修改类库中的任何现有类时不能删除任何现有的方法,因为那样肯定会破坏客户端程序员现有的代码,但是,还有一种情况更加棘手,在有域(类数据成员,也叫类变量)数据存在的情况下,类开发者要怎样才能知道究竟都有哪些域已经被客户端程序员调用了呢?这对于某些方法仅属于类的实现的一部分,因此并不想被客户端程序员直接使用的情况来说同样如此,如果类库的开发者想要移除旧的实现而添加新的实现时,又怎么处理呢?改动任何一个实现都有可能影响客户端程序员的代码,于是类库的开发者会束手束脚,无法对事物进行任何改动。
为了形象生动地说明问题,通过代码举例:
为了更好地说明问题,我暂时不用任何的权限修饰关键字
⑴、数据域、方法:

// 有数据域的代码举例
   //假设这个类是类库中的一个工具类。
   //类库工具类Test
   public class Test {
      //String类型的类变量s
	  String s;
	  //int类型的类变量i
	  int i;
	
	  //方法start()
	  void start() {}
	  //方法begin()
	  void begin() {}
}

实际代码示例:

java权限控制按钮 java权限控制原则_java权限控制按钮


客户端程序员的类:

需要使用类库的类实现某些功能。

java权限控制按钮 java权限控制原则_java权限控制按钮_02


客户端程序员使用完毕:

java权限控制按钮 java权限控制原则_Java_03


如果这个时候对类库中的类进行升级和修改:做了如下的改动:

改了一个方法的名称;

改了一个类变量的名称;

java权限控制按钮 java权限控制原则_java权限控制按钮_04


这个时候再来看下客户端程序员的代码发生了什么?

java权限控制按钮 java权限控制原则_包名_05


所以,如果没有进行控制,类库中的类一变动,立即就会对客户端程序员的代码产生不小的影响,最严重的可以导致代码无法启动运行!

其他的例子可以自行进行尝试,下面来重点讨论如何进行控制,如何避免上述情况的出现。

为了解决这一问题,Java提供了访问权限修饰词,以供类库开发程序员向客户端程序员指明哪些是可用的,哪些是不可用的(实际上这个不明显,因为你能看到的都是可用的,不可用的你根本看不到,除非你有兴趣看源码),访问权限控制的等级,从最大权限到最小权限依次为:publicprotected包访问权限(就是我上面代码示例中使用的权限,没有任何权限关键字修饰的就默认是包访问权限)和private------仅向客户端程序员公开你愿意让他们使用的方法和域,这样做是完全正确的。尽管对于那些使用其他的语言(尤其是C语言)编写程序并在访问事物的时候不受任何限制的人而言,这与他们的直觉相违背,但是,他们迟早还是会信服Java权限控制的价值。

可能你们已经注意到了,也是我为什么选择了这种权限作为示例代码的原因:包访问权限。从它这里又引出了一个"包"的概念

前面我们只讨论了与类有关的概念,但是,类一旦多了的话,怎么管理呢?或者换一个角度来看问题:会不会有一些类,有必要把它们放在一起,因为它们之间会相互依赖呢?

答案是:会的!

因此,我们会将一些依赖关系非常明显的类放在一起,也就是放在所谓的"包"中,同时,你需要给这个包取一个在世界上独一无二的名字。

你需要非常重视这个:在世界上独一无二的名字!如果接触过这块的就知道,因为域名在世界上就是独一无二的,所以Java中的包名通常就是域名或者是域名的倒序(如果你准备将你的程序发布供其他客户端程序员使用,最好是你自己的私人域名)。

那么,从这里需要引出一个很重要的编程思路:如果你身处Java语言编程行业,在实现一个功能的时候,你想找找是否有现成的类,你首先需要找的是包名(也就是类库的库名)。找到了之后,再进入包中找到目标类。通常,专业的编程网站上会经常发布一些类库,你可以自己关注关注。通常来说,包名就大概能展示这个包中的所有类是解决什么问题的。
下面继续Java编程思想第四版的内容
构件(不是构建,这里的"件"指的就是类)类库的概念以及对于谁有权取用该类库中的构件的控制问题还是不完善的,其中仍旧存在着如何将构件捆绑到一个内聚的类库单元中的问题,对于这一点,Java用关键字package加以控制。
而访问权限修饰词会因类是存在于一个相同的包,还是存在于不同的包而产生影响,所以,先理解如何将类库构件置于包中,才能更好地理解访问权限修饰词的全部含义!
- 1、包:库单元
Java之所以强大是因为它提供了很多现成的类!具体可以参考JDK文档。
链接: [ ]
有兴趣的话,最好是看英文版的,而且最好是最新版本的!

java权限控制按钮 java权限控制原则_包名_06


包内包含有一组类,它们在单一的名字空间之下被组织在了一起。

例如,在Java的标准发布库中有一个工具库,它们被组织在"java.util"名字空间之下。java.util中有一个叫做ArrayList的类,使用ArrayList的一种方式是用其全名java.util.ArrayList来指定:

// 使用全名来指定一个类
   //测试类FullQualification
   public class FullQualification{
      //程序的执行入口main方法
      public static void main(String[] args) {
         //使用全名来指定一个类:java.util.ArrayList
         java.util.ArrayList  list = new java.util.ArrayList();
      }
      //通过全名来告诉编译器,这里需要的是java.util包里的ArrayList类.
      //因为java.util这个包名是全世界独一无二的,ArrayList类在包java.util
      //里也是唯一的,所以不会产生混淆。
   }

实际代码示例

java权限控制按钮 java权限控制原则_java权限控制按钮_07


你会发现,这立刻就让程序变得冗长了,Java专门提供了一个import关键字用于你在程序中导入你需要的类,所以,在你的程序中你需要使用哪个类,可以直接在import语句中直接命名这个类:

使用了import关键字之后:

java权限控制按钮 java权限控制原则_java权限控制按钮_08


所有的Java代码,最顶部都是package关键字开头的包名(仅限一个),然后是import关键字开头的导入包名(可以没有,一个甚至多个)。

现在,你可以直接使用ArrayList了,而不需要再使用全限定类名了!

但是,仅仅是import java.util.ArrayList,java.util包中的其它类还是不可用的。

那么,会有一个问题:如果存在同时需要使用到一个包中的多个类,是不是每个类都需要使用import关键字导入一次?

答案是:不用!可以一次性导入一个包中的所有类。

Java提供了专门的语法,可以一次性导入一个包(也可以说是一次性导入包中的所有的类),然后在需要的时候随时使用这个包中的任意一个类!

具体语法是:import java.util.* ,这样你就导入了整个包,可以直接通过类名称使用这个包中的任何类:

代码示例:

java权限控制按钮 java权限控制原则_java权限控制按钮_09


之所以要通过导入,是为了提供一个管理名字空间的机制,防止出现两个完全相同的类名称,现在有了package机制,再多相同名称的类也会被隔离,不会产生冲突。(通过包名称隔离,因为包名称是世界上独一无二的,那么包名称+类名称的组合也是世界上独一无二的),在此基础上,所有类成员的名称也是彼此隔离的,就算两个类的名称完全相同,甚至它们的某一个方法名称完全相同,形式参数列表也完全相同都没有关系,因为都是可以区分的(同一个包中是不允许存在两个名称完全相同的类)。

可能你看到这里,你会突然想起你平时编程可能没有注意包名称,在这里,可以很肯定地说,就算你没有专门为类取一个包名称,它们实际上也是在包中了:即未命名包,也称默认包。这当然也是一种选择,完全是为了方便起见。如果你想编写与某台机器上的其他Java程序友好不发生冲突的类库或者程序的话,就需要考虑如何防止类名称冲突的问题。

当编写一个Java源代码文件时(此文件通常被称为编译单元,有时也被称为转译单元):

图示:

java权限控制按钮 java权限控制原则_java_10


这个源代码文件中,只能有一个public修饰的类:

图示:

java权限控制按钮 java权限控制原则_Java_11


同时,这个类的名称必须与文件的名称相同(包括大小写)。如果在该编译单元还有其他额外的类,那么在包之外的世界是没有办法看到它们的,因为它们不是public类,而且它们的主要作用就是用来支持主类的(主类就是用public修饰的那个类)。

图示:

java权限控制按钮 java权限控制原则_包名_12


再证明一下对包外的类不可见:

java权限控制按钮 java权限控制原则_包名_13


如果你在B包中想创建A包中的一个非public类的对象,那么编译器会报错,提示找不到这个类,因为它除了对A包中的类可见,对A包以外的其它类是不可见的(但是内部类是一个例外,如果内部类不用public修饰,对包外的其它类也是可见的!)!

⑴、代码组织:

当编译一个".java"文件时,这个文件中的每一个类都会有一个输出文件,每份文件的名称与".java"文件中的每个类名称是对应一致的,它们的后缀是".class"(可以这么总结:一个public类对应一个".java"文件,但是其中的每一个类都对应一个".class"文件)。因此,可能你在编译少量的".java"文件之后会得到大量的".class"文件,Java可运行程序是一组可以打包并压缩为JAR文档文件(JAR是使用Java的文档生成器生成的文件)的.class文件。Java解释器负责这些文件的查找,装载和解释(解释也就相当于是执行)。

原先的编译型语言,它会产生一个中间型文件(通常是一个obj文件),然后再与通过链接器(用于创建一个可执行文件)或类库产生器(librarian,用以创建一个类库)产生的其它同类文件捆绑在一起。

图示:

一个public类只产生了一个.java文件:

java权限控制按钮 java权限控制原则_java权限控制按钮_14


每个类都会产生一个.class文件:

java权限控制按钮 java权限控制原则_包名_15


对于.class文件,在后面熟悉了反射的原理之后,你肯定会对它的理解更加深刻!

类库实际上是一组类文件,其中每个文件都有一个public类,以及任意数量的非public类,如果你希望将一些类文件从属于同一个群组,就可以使用关键字package

如果使用package,它必须是代码文件中除注释以外的第一句程序代码,在文件的起始处写:如果你从来没有写过package,那么只有两种情况,一种是IDE默认给你写了,另外一个,你使用的是默认的空包空间。

比如这个群组的名字是"access"。

那么在程序的开头就应该这么写:

package access;

如果群组的名字是"com.baidu.www"。

那么在程序的开头就应该这么写:

package com.baidu.www;

图示:

也就是说,你的包名称前面加上package关键字就行了。

java权限控制按钮 java权限控制原则_java权限控制按钮_16


回到package access

就表示你在声明该编译单元是名为access类库的一部分,任何想要使用其中的类的人都必须通过指定全限定类名或者结合access使用import语句(注意:Java的包的命名规则是全部使用小写)。

比如:假设文件的名称是MyClass.java,这就意味着在该文件中有且只有一个public类,并且这个类的名称一定是MyClass(注意大小写)。

代码示例:

// .java文件名称必须和public修饰的类名称完全一致。
   //在access下面还可以继续增加文件的层次,
   //这里在access文件下面又新增了一个mypackage文件夹。
   package.access.mypackage
   //public修饰的类,名称和文件名称完全一致。
   public class MyClass{}

现在,如果想使用MyClass。或者是access下面的任何public修饰的类(注意这里强调了是public修饰的类!),就必须使用import关键字(另一个选择是指定全限定类名)。
代码示例:

// 指定全限定类名
   //类QualifiedMyClass
   public class QualifiedMyClass{
      //程序执行入口main方法
      public static void main(String[] args){
        //通过全限定类名指定具体的类
        access.mypackage.MyClass m = new 
        access.mypackage.MyClass();
      }
   } 
 //------------------------------------
 //通过import关键字
   //import关键字导入需要使用的类的包名称。
   //一般都是通过通配符"*"导入整个包.
   //如果你能确定你需要使用的类,多次导入也可以
   //比如import access.mypackage.MyClass;
   import access.mypackage.*;
   //类QualifiedMyClass
   public class QualifiedMyClass{
      //程序执行入口main方法
      public static void main(String[] args){
        //仅需要类名即可
        MyClass m = new MyClass();
      }
   }

package和import关键字做的是将单一的全局名字空间分隔开(说白了就是为了有更多的名称可以使用而已!),使得无论多少人使用Internet以及Java开始编写类,都不会出现名称冲突的问题!
⑵、创建独一无二的包名:
你可能已经发现,如果将很多个.class文件放在一个包中,情况往往会变得复杂,为了避免这种情况发生,一种合乎逻辑的做法就是将特定的所有.class文件都置于同一个目录中,也就是说,可以利用操作系统的层次化文件结构来解决这一问题(这是Java解决文件混乱的一种方式,JAR工具还有另外一种方式)。
将所有的特定的文件都收录在一个子目录中还能解决另外两个问题:怎样创建独一无二的包名称以及如何查找可能隐藏于目录结构中的类,从目前的解决办法来看,是通过将.class文件所在的路径编码成package的名称来实现的。
在前面已经提过了,package的第一部分一般是反序的Internet域名(因为每个域名都是世界上独一无二的),保证全世界的类名称不会发生冲突。
如果你打算发布Java代码,去申请一个域名还是很有必要的!
编译完毕后,Java代码中的package名称将被分解为机器上的一个目录,当Java程序运行的时候,就能通过具体的路径(文件的目录)找到需要的.class文件。
Java解释器的运行过程如下:
首先,找出环境变量CLASSPATH(可以通过操作系统来设置,有时也可通过安装程序–用来在你的机器上安装Java或基于Java的工具来设置),CLASSPATH包含一个或者多个目录,用作查找.class文件的根目录,从根目录开始,解释去获取包的名称,并将每个句点替换成反斜杠(也就是将".“替换成”"),以从CLASSPATH根中产生一个路径名称(比如 package foo.bar.baz就变成foo\bar\baz或foo/bar/baz或其他,结果视具体的操作系统而定),得到的路径再与CLASSPATH中的项相连接,解释器就在这些目录中查找你所要创建的类相关的**.class**文件(解释器可能还会去查找某些涉及Java解释器所在位置的标准目录)。
举例:
以Java编程思想作者的域名"MindView.net"为例,将它变成小写:"net.mindview"就成了一个独一无二的包名,若再创建一个名为simple的类库,于是得到了一个包名称如下:
package net.mindview.simple;
现在,可以在它们下面创建类了:

// 记得package语句一定是除注释以外的第一句代码
  //包名称
  package net.mindview.simple; 
  //类Vector
  public class Vector{
     //构造方法
     public Vector(){
        //打印字符串"net.mindview.simple.Vector"
        System.out.println("net.mindview.simple.Vector");
     }
  }
  //第二个文件也极其相似
  //包名称
  package net.mindview.simple; 
  //类List
  public class List{
     //构造方法
     public List(){
        //打印字符串"net.mindview.simple.Vector"
        System.out.println("net.mindview.simple.List");
     }
  }

假设这两个文件放置在子目录"C:/DOC/JavaT/net/mindview/simple"下面。
在这个路径里,你能看到包的名称net.mindview.simple,但是此路径的第一部分怎么处理呢?

答案是:将它放进环境变量CLASSPATH下面

你可以找到你自己机器上的CLASSPATH,我机器上的是:

CLASSPATH=.;D:\JAVA\LIB;\C:\DOC\JavaT

建议每个人在你的机器上的CLASSPATH路径的最前面都写上".;",尽管目前的技术已经发展到了可以不用专门去配置CLASSPATH了!
可以看到CLASSPATH可以包含多个可供选择的查询路径。
但是在使用JAR文件的时候会有一点变化,必须将JAR文件的全部名称都写清楚,包括文件后缀,而不仅仅是指明它所在位置的目录,因此,对于一个名为grape.jar的JAR文件,它在CLASSPATH中应该这么写:

CLASSPATH=.;D:\JAVA\LIB;\C:\flavors\grape.jar

说到这里,你可以回想一下之前使用Java编程的时候,配置CLASSPATH时涉及到的rt.jar,然后你再通过百度了解一下这个rt.jar是干什么的,就都明白了!
一旦类路径正确,就可以开始编程了:

// CLASSPATH路径正确后,可以直接通过import使用目标类。
   //导入目标类(保证你已经在CLASSPATH中已经配置好了这个路径).
   improt net.mindview.simple.*;
   //类LibTest
   public class LibTest{
      //程序执行入口main方法
      public static void main(String[] args){
          //分别创建类对象
          Vector vector = new Vector();
          List list = new List();
      }
   }

这里需要提一句:LibTest.java文件可以放在你机器上的任意位置!
编译器首先从import语句开始,遇到的是net.mindview.simple,就开始在CLASSPATH指定的目录中查找,查找子目录net\mindview\simple,然后从已编译的文件中(也就是.class文件)找出名称相符者(Vector就是Vector.class,List就是List.class),所以你必须保证类Vector以及类List都是public的。
对于使用Java的新手而言,设置CLASSPATH是一件很麻烦的事情,因此,sun在JDK2中进行了一些改进,安装了JDK以后,及时你未设置CLASSPATH,你也可以编译运行基本的Java程序,但是,如果你需要其他的Java代码的支持,你还是需要在CLASSPATH里面进行配置。
这里再提一个小问题:使用通配符可能产生的冲突
使用通配符"*"可能存在一个潜在的冲突:
比如:

// 通配符"*"可能产生的冲突
   //导入net.mindview.simple所有的类
   import net.mindview.simple.*;
   //导入java.util所有的类
   import java.util.*;

由于java.util包里也有一个Vector类,这就存在潜在的冲突:
如果创建一个Vector类:
Vector v = new Vector();
那么,到底应该取用哪个包下面的Vector类呢?编译器肯定不知道,这时,编译器会报错,提示必须指明到底应该使用哪个包下面的Vector类。如果你需要一个标准的Vector类:
java.util.Vector v = new java.util.Vector();
如果不是:
net.mindview.simple.Vector v = new net.mindview.simple.Vector();
通过这样就能指明Vector类的具体位置(配合CLASSPATH)。
或者你可以使用导入单个类的形式,否则你只能通过使用全限定类名来指定你需要使用的那个具体的类。
⑶、定制工具库:
了解了以上的知识点后,你可以创建自己的工具库(达到减少甚至是消除重复代码的目的):

由于这里需要说明的内容太多,我专门写了一篇博文

如果有兴趣,请参看博文:
JavaSE基础知识(十六)–Java环境变量配置(Windows黑窗口如何执行Java程序,CLASSPATH,PATH如何配置),以及为什么需要配置环境变量
在后面的编程中,你可以将更多的类放入自己的类库中。
⑷、用import改变行为:
Java没有C的条件编译功能,该功能可以使你不必更改任何程序代码,就能在不同的代码行为之间进行切换,Java去掉此功能的原因可能是因为C在绝大多数情况下是用此功能来解决跨平台问题的,即程序代码的不同部分是根据不同的平台来编译的。由于Java自身可以自动跨越不同的平台,因此这个功能对于Java来说是没必要的。
然而,条件编译还有一些其他有价值的用途,比如调试,调试功能在开发的过程中是开启的,而在发布的产品中是禁用的,可以通过修改导入的package来达到这个目的,修改的方法是将你程序中的调试版本改成发布版本,这一技术可以用于任何种类的代码。
比如你创建了两个包:debug和debugoff,它们都包含一个相同的类dg,该类有一个相同的方法debug(),第一个版本是发送给控制台的String参数,而第二个版本什么也不做。你可以通过修改package的导入,在这两个方法之间进行切换。
⑸、对使用package的忠告:
务必记住,无论何时创建包,都已经在给定包的名称的时候,隐含地指定了目录的结构,这个包必须位于其名称指定的目录之中,而该目录必须是在以CLASSPATH开始的目录中可以查询到的,最初使用package的时候,可能会有一点不适应,因为除非遵守"包的名称对应目录路径"的规则,否则你将会收到许多出乎意料的运行时信息,信息往往提示你找不到主类或特定的类,哪怕是这个类就位于同一个目录之中,如果你收到了类似的信息,尝试将package注释掉,如果程序能运行,那么你就知道问题出在哪里了。
注意,编译的.class文件,经常与.java源码文件不放在一起,但必须保证通过CLASSPATH可以找得到该路径。
如果,你对上面这段文字理解不了,那么你可以参看我的博文:JavaSE基础知识(十六)–Java环境变量配置(Windows黑窗口如何执行Java程序,CLASSPATH,PATH如何配置),以及为什么需要配置环境变量 里面有两个例子,一个例子是没有配置CLASSPATH就把程序运行起来了,还有一个配置了CLASSPATH也把程序运行起来了,但是配置了CLASSPATH的例子中,是增加了package关键字的。你可以尝试在没有配置CLASSPATH中的那个例子中加上一个package,内容就是它的.class的当前存储目录(也就是当前目录),你会发现,系统提示找不到主类或特定的类,其实就是说明了,package关键字的内容对CLASSPATH有决定性的影响,也可以说对找到需要执行的.class文件有决定性的影响!
基础内容就这些了,下一篇博文将专门讲述Java权限的关键字。