目录
库单元:package
代码组织
独一无二的包名
Java访问权限修饰符
包访问权限
接口访问权限(public)
不可访问(private)
继承访问权限(protected)
包访问权限与公共构造器
接口与实现
类的访问权限
新特性:模块
本笔记参考自: 《On Java 中文版》
对于一些项目而言,大部分的时间和金钱并不是消耗在编程阶段,而是消耗在了之后的维护阶段。重构存在的一个主要原因,就是为了重写那些可以正常工作的代码,提高其的可读性、可理解性和可维护性。
在实际进行开发的过程中会发现,有些库的用户希望能依赖于他们目前使用的那部分代码,而库的创建者却要求能自由修改和完善自己的代码。为了解决这一矛盾,Java提供了访问权限修饰符,包括:public、protected、包内访问(默认权限,无关键字)和private。在此之上,为了能够将类进行归纳、打包,Java还提供了package关键字。
库单元:package
一个包内含了一组类,这些类由同一个命名空间组织在一起。例如,使用Java提供的类ArrayList,该类就被放置在命名空间java.util中,可以通过两种方式进行用:
// 方法一:指定全名
java.util.ArrayList list = new java.util.ArrayList();
而为了使代码看起来更加简洁,可以使用import关键字:
// 方法二:使用import关键字
import java.util.ArrayList;
public class SingleImport {
public void main(String[] args) {
ArrayList list = new ArrayList(); // 可直接使用
}
}
也可以使用“*”导入java.util的其他类:import java.util.*;
上述的这种导入提供了一种管理密码空间的机制:通过完全控制Java中的命名空间,为每一个类创建唯一的标识符组合。(所以,包名的设计也是需要经过考虑的。)
一个Java源代码文件就是一个编译单元(又称转译单元)。对一个编译单元,有几点限制:
- 每个编译单元中只能有一个public类;
- public类的名字必须与文件同名(包括大小写,但不包括文件扩展名)。
若一个编译单元有除了public类之外的其他类,那么在该编译单元所处的包外是无法发现这些类的,因为这些类只是public类的支持类。
代码组织
编译一个.java文件时,文件中的每一个类都会生成一个输出文件(扩展名是.class)。输出文件的名字就是其在.java文件中对应类的名字。这些.class文件可以通过jar归档器打包成一个Java档案文件(JAR),JAR通过Java解释器进行使用。
库,就是一组如上所述的类文件(.class文件)。每个源文件通常都由一个public类和任意数量的非public类组成,因此每个源文件中都有一个公共组件。可以使用package关键字把这些组件打包到一个命名空间中。
package语句必须出现在文件的第一个非注释处,形如:
package bagname; // bagname就是包名,在本地中可以理解为文件所处的文件夹的名字
上述语句将一个编译单元包括到了bagname这个库中。同时,这个编译单元中如果存在public类,那么要调用这个类,就必须先使用bagname这一命名空间。
现在,假设在一个项目中存在着文件MyClass.java,其存在于路径test/hiding/mypackage中:
package hiding.mypackage;
package hiding.mypackage;
public class MyClass {
private static MyClass m;
MyClass() {
// ...
}
public static MyClass Make() {
System.out.println("Hello, MyClass test");
return m;
}
}
若要使用MyClass,就必须使用import关键字使得example.mypackage中的名称可用。例如:
// 位于文件夹test/hiding中
package hiding;
import hiding.mypackage.*; // [1]
public class QualifiedMyClass {
public static void main(String[] args) {
MyClass m = new MyClass(); // [1]
// example.mypackage.MyClass m = new example.mypackage.MyClass(); // [2]
}
}
其中,[1]和[2]使用其一即可。Java通过package和import关键字来分隔命名空间,防止冲突。
此处将QualifiedMyClass.java文件放入了hiding包内。若不进行打包,则在进行编译或运行时,可能会遇到NoClassDefFoundError错误。另外,进行打包后,类的全称会发生改变,例如MyClass的全称就是hiding.mypackage.MyClass(若使用java指令运行MyClass时,需要使用全称,并且建议在test文件夹中运行)。
独一无二的包名
为了整合包所有的组件,一个好的办法是将属于该包的所有.class文件放到一个目录中(利用好操作系统的分层结构)。这种方式解决了两个问题:①创建唯一的包名;②寻找可能隐藏在目录结构中的类。
按照惯例,package的名称通常由两个部分组成:
- 第一部分,由创建者的反向的因特网域名构成;
- 第二部分,由机器上的目录组成。
例如,将mp.csdn.net进行颠倒,就会得到net.csdn.mp。通过使用net.csdn.mp,我们就可以得到一个用于唯一认定自己的类的全局名称。现在可以创建一个simple库,就可以进一步细分,得到以下的包名:
package net.csdn.mp.simple;
像这种的包名就可以被用作命名空间,用来保护位于其中的类:
// 包名的第二部分就是 net.csdn.mp.simple
// 创建一个包
package net.csdn.mp.simple;
public class Vector {
public Vector() {
System.out.println("这个类的全名应该是:net.csdn.mp.simple.Vector");
}
}
这个组件可以通过下列语句进行引用:
import net.csdn.mp.simple.Vector;
假设上述的Vector.java源文件在本地中储存的位置是:
/home/user/Java/test/net/csdn/mp/simple
上述路径的后半部分net.csdn.mp.simple组成了包名。而路径的第一部分则由CLASSPATH环境变量提供。为此,就需要在本地机器中,为CLASSPATH添加路径(由于笔者使用的是Ubuntu系统,故提供当前系统下添加路径的方法):
export CLASSPATH=/home/user/Java/test
进行这种类型的路径添加,一个主要的好处是可以在当前的包外使用自定义的库。例如,当添加了上述路径时,可以在与test平行的目录other中,使用MyClass类:
// 位置为:other/HelloWorld.java
import net.csdn.mp.simple.Vector;
public class HelloWorld {
public static void main(String[] args) {
Vector v = new Vector();
}
}
可以运行程序,得到下列输出:
冲突
若通过*导入的两个库中包含了相同的名称,例如:
import net.csdn.mp.simple.*; // 包含了一个自定义的Vector类
import java.util.*; // 包含了标准库中的Vector类
此时,若按照如下方式创建Vector类,就有可能发生冲突:
Vector v = new Vector();
尝试编译,发生报错:
正确的方式是对需要使用的Vector类进行指定:
java.util.Vector v = new java.util.Vector();
因此,在这种情况下就不需要通过import关键字进行导入,除非还使用了库中的其他内容。或者,如果使用单类导入的形式,也可以避免发生冲突。
Java不存在C语言拥有的条件编译,这是因为Java能够自动跨平台。但有时,为了进行调试,还是需要使用条件编译的,为此可以通过package关键字改变导入的包,将程序中使用的代码从调试版本切换到生产版本,以此实现条件编译的功能。
Java访问权限修饰符
Java的访问权限修饰符包括:public、protected和private。这些修饰符放在类中成员(包括字段和方法)定义的前面,控制被修饰定义的访问。而若不使用访问权限修饰符,成员会拥有默认的“包访问权限”。
包访问权限
默认访问权限没有关键字,通常称为包访问权限(又称“友好访问权限”)。这种权限只允许当前包中的所有其他类访问该成员,而对此包之外的所有类,该成员显示为private(处于隐藏状态)。由于一个编译单元只属于一个包,所以一个编译单元中的所有类都可以通过包访问权限进行相互访问。
包访问权限的存在,要求将类分组到一个包中。所以,在Java中,以合理的方式组织文件中的定义方式是重要的。
类控制着那些代码可以访问其成员。授予成员访问权限的几个方法如下所示:
- 将成员设置为public,这样就允许任意代码访问该成员;
- 不为成员添加任何访问权限修饰符——赋予成员包访问权限。这样处于同一个包内的其他类就可以访问该成员;
- 当使用继承时,子类可以访问父类的protected成员和public成员,若两个类处于同一个包中,子类可以进一步访问父类的包访问权限成员;
- 提供可以读取和更改值的访问器(accessor)和修改器(mutator)方法。
接口访问权限(public)
若使用public修饰,这意味着其之后的成员对所有人而言都是可用的,包括开发者和客户。假设存在一个包含了以下编译单元的desert包:
// 位于目录 /example/hiding/desert 中
package hiding.desert;
public class Cookie { // 具有public权限,可从包外进行访问
public Cookie() {
System.out.println("这是一个类Cookie的构造器");
}
void bite() { // 默认的包访问权限,无法从包外进行访问
System.out.println("随便写点什么");
}
}
记得将hiding目录设置为CLASSPATH指定的路径之一。使用语句javac -classpath ./ xxx.java可以在编译时将CLASSPATH设定到当前目录。
现在可以让其他程序使用类Cookie了:
// 位于example/中
package example;
// 位于目录 /hiding 中
import hiding.dessert.*;
public class EatingTheFood {
public static void main(String[] args) {
Cookie c = new Cookie();
}
// c.bite(); // 无法访问
}
程序运行的结果如下:
在上述的Cookie类中,由于其的构造器和类都是public的,所以它的对象可被创建。但方法bite()只有包访问权限,所以在其包外是无法进行访问的。
默认包
若两个类处于同一目录中,那么即使这两个类没有明确的包名,它们也可以进行相互的访问。例如:
// 位置是hiding/Cake.java
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
}
下面的文件Pie.java和Cake.java处于同一目录中:
// 位置是hiding/Pie.java
class Pie {
void f() {
System.out.println("这条语句存在于Pie.java中");
}
}
被放在同一目录中的类会被视为属于当前目录的“默认包”的隐含部分。因此,这种文件会为该目录中的其他文件提供包访问权限。
不可访问(private)
若一个类存在使用private修饰的成员,那么除了包含此成员的类及其成员之外,其他任何类不可访问该成员(即使是同一个包中的其他类)。通常,可以自由修改和替换通过private修饰的成员(虽然隐藏可以通过反射进行规避)。
特别是在涉及多线程时,使用private是十分重要的。
class Sundae {
private Sundae() { // 该构造器被隐藏,无法从Sundae外被调用
System.out.println("一个被隐藏的Sundae构造器");
}
static Sundae makeASundae() {
return new Sundae(); // 在Sundae类中,拥有调用构造器Sundae()的权限
}
}
public class IceCream {
public static void main(String[] args) {
Sundae x = Sundae.makeASundae(); // 通过调用静态方法,可以通过指定构造器进行对象创建
}
}
程序运行的结果如下:
上述程序展示了private的一个用法:控制对象的创建方式,防止特定的构造器(或所有的构造器)被调用。
若确定某方法是类的“辅助”方法,将其设为private能够为后期的维护与修改保留选择。字段也是如此,除非需要公开底层实现,否则最好将字段设为private。
继承访问权限(protected)
protected关键字多被用于处理继承的概念。通过继承,可以通过一个现有类(即基类),在不修改现有类的情况下向类中添加新成员,或改变现有成员的行为。使用extends声明新类继承了现有类:
class SubClass extends BaseClass { // ...
子类对基类的访问权限有几种:
- 子类可以访问基类的public成员和protected成员;
- 若子类和基类位于同一个包中,子类可以访问基类所有的包访问权限成员。
protected关键字也提供了包访问权限。其与public的区别在于,若不在同一个包中,那么包外的类可以访问public成员,但是不可以访问protected成员。
以之前提到的Cookie.java为例,它的bite()方法只有包访问权限:
void bite() { // 默认的包访问权限,无法从包外进行访问
System.out.println("随便写点什么");
}
但如果有子类需要访问该方法,那么就必须改变bite()的权限。public允许所有的访问,这或许不会是我们想要的。为此,就需要使用protected关键字了:
// 位于目录 /hiding/dessert 中
package hiding.dessert;
public class Cookie { // 具有public权限,可从包外进行访问
public Cookie() {
System.out.println("这是一个类Cookie的构造器");
}
protected void bite() { // 默认的包访问权限,无法从包外进行访问
System.out.println("随便写点什么");
}
}
这样就可以让任何继承Cookie的类访问bite()了:
// 位置是hiding/ChocolateChip.java
package hiding;
import hiding.dessert.Cookie;
public class ChocolateChip extends Cookie {
public ChocolateChip() {
System.out.println("这是ChocolateChip的构造器");
}
public void chomp() {
bite(); // 使用了Cookie的protected方法
}
public static void main(String[] args) {
ChocolateChip c = new ChocolateChip();
c.chomp();
}
}
程序运行的结果是:
包访问权限与公共构造器
若一个类只具有包访问权限,现在给这个类一个public构造器,会发现编译器不会进行任何报错:
// 位置是hiding/packageaccess/PublicConstructor.java
package hiding.packageaccess;
class PublicConstructor {
public PublicConstructor() { // ...
}
}
但这不代表没有问题,因为这是一个虚假陈述——实际上无法从包外访问这个public构造器。如果尝试从外部对其进行调用,就会发现报错:
// 位置是hiding/CreatePackageAccessObject.java
package hiding;
import hiding.packageaccess.*;
public class CreatePackageAccessObject {
public static void main(String[] args) {
new PublicConstructor();
}
}
发生了报错:
接口与实现
访问控制也被称为实现隐藏。将数据和方法包装在类中,并与实现隐藏相结合,称为封装。其结果就是具有特征和行为的数据类型。
访问权限控制在数据类型的内部设置了访问边界:
- 确定客户程序员可以使用和不可使用的内容;
- 将接口和实现分离。
类的访问权限
访问权限修饰符还决定了库内部的那些类可以提供给用户使用。要控制对类的访问,访问权限修饰符就必须出现在关键字class前面:
public class Weight { // ...
这种类的使用之前已经多次进行过展示,这里就不再赘述了。值得一提的是,在进行类的设计时会额外添加一些限制:每个编译单元(即文件)都只能有一个public类,且该类的名字必须和文件名完全一致。
当然,编译单元中可以没有public类。这种类通常都是用来完成一些其他public类分发的任务,不使用public可以让这种类隐藏在包中,这样可以让实现变得更加灵活。
关于赋予权限,有这样的一些建议:
- 应该尽量将字段设置为private权限;
- 方法应该具有和类相同的访问权限(包访问权限);
- 类不应该是private或protected的,类的访问权限有两种:包访问权限和public。若想防止对该类的访问,可以将其构造器全部设置为private(转而使用静态方法创建对象)。
class Soup1 {
private Soup1() { // 不允许外部访问构造器
System.out.println("隐藏的Soup1构造器");
}
public static Soup1 makeSoup() {
return new Soup1(); // 使用静态方法返回对象
}
}
class Soup2 {
private Soup2() {
System.out.println("隐藏的Soup2构造器");
}
private static Soup2 s2 = new Soup2(); // 声明静态对象
public static Soup2 access() {
return s2;
}
public void f() {
System.out.println("被Soup2对象调用的f()");
}
}
public class Lunch {
void testPrivate() {
// Soup1 soup1 = new Soup1(); // 编译报错,Soup1是隐藏的
}
void testStatic() {
Soup1 s = Soup1.makeSoup();
}
void testSingleon() {
Soup2.access().f();
}
public static void main(String[] args) {
Lunch lh = new Lunch();
lh.testStatic();
lh.testSingleon();
}
}
程序执行的结果如下:
上述程序中,类Soup2使用的是单例模式,从始至终只创建一个对象。
新特性:模块
在JDK 9之前,Java程序的运行会依赖整个Java库。这意味着,哪怕只是使用库的一个组件,编译器也会将整个Java库包含在里面。
JDK 9引入了模块的概念:将代码划分为一个一个的模块。由这些模块指定它们依赖的模块,并且定义可用或者不可用的模块。在此之后,若使用库组件,就只会获得对应的模块及其的依赖项,这就省去了不必要的导入。
另外,使用“逃生舱口”(escape hatch)可以调用隐藏的库组件,但因此产生的可能问题需要由程序员自己负责。
为了更好地探索这个新的系统,Java提供了新的命令行标识,比如:
- 显示所有可用模块:
java --list-modules
-
若要查看模块的内容,例如base模块,可以使用命令:
java --describe-module java.base
模块系统多被用于大型的第三方库。