文章目录
- 6.1 包:库单元(the library unit)
- 6.1.1 代码组织
- 6.1.2 创建独一无二的包名
- 练习1
- 练习2
- 6.1.3 定制工具类
- 6.1.4 用import改变行为
- 练习3
- 6.1.5 对使用包的忠告
- 6.2 Java访问权限修饰词
- 6.2.1 包访问权限
- 6.2.2 public:接口访问权限
- 6.2.3 private:你无法访问
- 6.2.4 protected:继承访问权限
- 练习4
- 练习5
- 练习6
- 6.3 接口和实现
- 6.4 类的访问权限
- 练习7
- 练习8
- 练习9
Access control (or implementation hiding) is about “not getting it right the first time.”
通常类库开发者需要不断重构代码,来不断完善和改进程序。但是一般对应的接口代码应该尽量保持不变。类库开发者怎样才能知道哪些程序会别前端调用?这就需要将哪些不想让前端掉用的程序和需要调用的区分开来。
解决这个问题的方法就是使用权限访问控制。用不同的权限修饰符来表示不同的访问权限等级,从最大权限到最小权限为:
public
、protected
、包访问权限(默认,没有关键字)、private
。
Java使用package
,来管理类名的命名空间。package将功能相似的类组织到同一类中,便于管理。即使同一名字的多个类出现,只要它们在不同的package
中,就就没有名字冲突。package
也有访问权限,某个类需要包访问权限才能访问其他包中的类。
6.1 包:库单元(the library unit)
一个java源代码文件,叫做一个编译单元。每个编译单元必须且只能有一个public
修饰的类。这个类名必须和文件名一模一样(区分大小写,不包括.java
后缀名)。
6.1.1 代码组织
使用javac
对一个.java
文件编译时,这个文件里面的所有类,都会产生一个.class
字节码文件。和C/C++不一样,Java的可运行程序是一组可以打包并压缩为一个Java文档文件(JAR,使用Java的 jar文档生成器)的.class
字节码文件。Java解释器负者对这些文件的查找、装载和解释(Java中并不要求必须使用解释器,因为存在用来生成一个单一的可执行文件的本地代码Java编译器)。
类库是一组class文件。每个文件中都有一个public
的类和任意数量的非public
的类。通过package可以将他们放到一个组中。
6.1.2 创建独一无二的包名
将某个包的所有.class
文件都放到同一个目录下,利用操作系统的层次化的文件结构类解决,多个类库中许多.class
文件
Java解释器的运行过程:
- 首先,当编编译器碰到
import
语句时,就开始从CLASSPATH
目录中查找。将环境变量CLASSPATH
作为查找.class
文件的根目录。CLASSPATH
一般包含一个或多个目录。 - 解释器从根目录开始,获取包名称并将每个句点替换为反斜杠,然后将根路径和这个路径拼接到一块。然后到这个目录下查找相关的
.xlass
字节码文件。例如CLASSPATH=/root/code
,那么解释器将/root/code
作为根目录,去这个目录里找包,加入找到一个名为foo.bar.baz
的包,然后就把foo.bar.baz
转换为foo/bar/baz
,然后将它和根目录拼接到一块为/root/code/fo/.bar/baz
,然后到/root/code/fo/.bar/baz
目录下查找相关的.class
文件。
package
语句必须是文件中的第一行非注释程序代码。
CLASSPATH
对于JAR文件,需要连写文件命由写秦楚。
练习1
package com.qww.exec01.simple;
public class Vector { }
package com.qww.exec01;
import com.qww.exec01.simple.Vector;
public class Exec01 {
public static void main(String[] args) {
Vector vector = new Vector();
}
}
冲突
如果导入方式是:
import com.qww.exec02.simple.*;
import java.util.*;
那么在使用Vector
类时,就会有冲突。
编译器提示错误:Error:(9, 9) java: 对Vector的引用不明确 java.util 中的类 java.util.Vector 和 com.qww.exec02.simple 中的类 com.qww.exec02.simple.Vector 都匹配
练习2
package com.qww.exec02;
import com.qww.exec02.simple.Vector;
import java.util.*;
public class Exec01 {
public static void main(String[] args) {
Vector vector = new Vector();
}
}
6.1.3 定制工具类
public class PrintUtil {
private static final PrintStream out = System.out;
public static void println(Object obj) {
out.println(obj);
}
public static void println(int x) {
out.println(x);
}
public static void println(Long x) {
out.println(x);
}
public static void println(float x) {
out.println(x);
}
public static void println(double x) {
out.println(obj);
}
public static void println(String s) {
out.println(obj);
}
}
package com.qww.test01;
public class Range {
public static int[] range(int n) {
return range(0, n);
}
public static int[] range(int start, int end) {
return range(start, end, 1);
}
public static int[] range(int start, int end, int step) {
int size = (end - start) / step;
int[] ret = new int[size];
for (int i = 0; i < ret.length; i++) {
ret[i] = start + i * step;
}
return ret;
}
}
6.1.4 用import改变行为
java没有C的条件编译问题。因为C的条件编译功能是解决跨品台问题的。
条件编译还有其他用体:调试程序,调试功能在开发时开启,在发布时禁用。可以通过修改导入的package来实现。修改方法是将程序中拥到的代码从调试版改为发布版,
练习3
package com.qww.exec03.debug;
public class A {
private int a;
public static void debug(String s) {
System.out.println(s);
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
package com.qww.exec03.debugoff;
public class A {
private int a;
public static void debug(String s) {}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
package com.qww.exec03;
import com.qww.exec03.debug.A;
// import static com.qww.exec03.debug.A.*;
import static com.qww.exec03.debugoff.A.*;
public class Exec03 {
public static void main(String[] args) {
A a = new A();
a.setA(10);
debug(a.getA() + "");
}
}
6.1.5 对使用包的忠告
无论什么时候创建包,都已经将在给定包的名称中的时候隐含地指定了目录结构(包名称对应目录结构)。该目录必须在以CLASSPATH
开始的目录中可以查找到。如果出现错误说找不到某个类,或者类似信息,试试注释掉package
语句。
通常编译的代码和源代码不会放在同一个目录,但是也必须让JVM通过CLASSPATH
可以找到编译的代码。
6.2 Java访问权限修饰词
public
、protected
、没有权限修饰符(包访问权限)、private
可以修饰方法、字段的定义。
6.2.1 包访问权限
默认访问权限没有任何权限修饰符,是指包访问权限(有时用friendly表示)。意味着当前包中的其他类都可以访问,但是这个包外面的类不能访问。因为一个编译单元(也就是写在一个文件中的多个类),只能属于一个包,所以同一个文件内的没有修饰多个类可以互相访问。
在同一个包中的其他类如何访问这个类的成员呢?
- 使用
public
修饰这个类的成员。public
修饰的字段或方法,无论在哪里都可被其他类访问。 - 将这个类的成员的权限修饰符改为默认的包访问权限,或者通过子类继承这个类,且这个类的成员的修饰符为
public
或protected
。子类可以访问父类中public
或protected
的成员,但不能访问private
修饰的成员。只有两个类都在同一个包中,才可以访问包访问权限的成员。 - 在这个类中编写“accessor”和“mutator”f方法(也叫Get和Set方法)。
6.2.2 public:接口访问权限
package com.qww.test02.access.dessert;
public class Cookie {
public Cookie() {}
void bite() {}
}
package com.qww.test02.access;
import com.qww.test02.access.dessert.Cookie;
public class Cake {
public static void main(String[] args) {
Cookie cookie = new Cookie();
// Error:(12, 17) java: bite()在com.qww.test02.access.dessert.Cookie中不是公共的; 无法从外部程序包中对其进行访问
// cookie.bite();
}
}
Cookie.java
在com.qww.test02.access.dessert
中,而Cake.java
在com.qww.test02.access
包中,public
修饰Cookie()
构造器并且Cookie
类也是puvlic
的。所以Cake
类中是有权访问Cookie()
构造器的。而bite()
方法被默认包访问权限所修饰,Cookie
和Cake
不在同一个包中,Cake
是没有权限访问bite()
的。
默认包
// Pie.java
class Pie {
void f() {
System.out.println("f()");
}
}
// Cake.java
class Cake {
public static void main(String[] args) {
Pie pie = new Pie();
pie.f();
}
}
// 运行结果
f()
看起来这两个文件(A.java
和B.java
)好像毫不相关。但是可以Cake中可以通过创建的Pie对象调用f()方法。原因是Cake.java和Pie.java在同一个相同的目录里,并且我们没有改给它们设定任何包名。java会将这样的文件看做是该目录的默认包中。也就都有了包访问权限,从而可以互相访问。
6.2.3 private:你无法访问
private
修饰的成员,只有本类可以访问,其他类都没哟权限访问。因此可以随意改变本尅的成员,而不用担心会影响到其他类。
package com.qww.test03;
public class Test03 {
public static void main(String[] args) {
// Error:(6, 15) java: A() 在 com.qww.test03.A 中是 private 访问控制
// A a = new A();
A a = A.getInstance();
}
}
package com.qww.test03;
public class A {
private A() {}
static A getInstance() {
return new A();
}
}
private
可以控制如何创建对象,并阻止别人方法某个特定的构造器(或全部构造器)。A
的构造器是private
的,并且A只有这一个无参构造器,所以它还可以阻止别的继承它。
不能因为在类中的某个对象的引用是private
,就认为其他类的对象无法拥有该对象的public
引用。
6.2.4 protected:继承访问权限
protected
关键字处理的是继承的访问权限,继承要解决的问题:通过extends来扩展一个基类,而不需要修改这个类。子类会继承父类的所有成员。
如果创建了另一个新的包,一个包里吗有父类,而另一个包里面有子类,那么子类可访问的成员是父类中public
修饰的成员。protected
解决的问题:在不同的包内,父类需要为所有子类提供其特定成员的包访问权限。相同包内的其他类可以访问protected
修饰的成员、
package com.qww.test04.cookie;
public class Cookie {
protected void bite() {
System.out.println("bite()");
}
}
package com.qww.test04;
import com.qww.test04.cookie.Cookie;
public class ChocolateChip extends Cookie {
public void chomp() {
/*
bite()方法没有权限修饰符修饰时
Error:(8, 10) java: 找不到符号
符号: 方法 bite()
位置: 类 com.qww.test04.ChocolateChip
*/
bite();
}
public static void main(String[] args) {
ChocolateChip x = new ChocolateChip();
x.chomp();
}
}
package com.qww.test04.cookie2;
import com.qww.test04.Cookie;
public class ChocolateChip2 extends Cookie {
public void chomp() {
bite();
}
public static void main(String[] args) {
ChocolateChip2 x = new ChocolateChip2();
x.chomp();
}
}
Cookie类中有一个默认权限访问的方法,并且子类ChocolateChip和父类Cookie不在同一个包内,虽然子类会将父类的字段和方法都继承了过来,但是因为父类bite()方法包访问权限的,所以子类是无法访问bite()的。这就需要父类使用protected
关键字为所有子类提供包访问权限。只有只有不同包的子类或者同一个包中的其他类可以访问bite()方法了。
练习4
package com.qww.exec04.hello2;
import com.qww.exec04.BaseClass;
public class Exec04 {
public static void main(String[] args) {
BaseClass baseClass = new BaseClass();
// Error:(14, 19) java: f() 在 com.qww.exec04.BaseClass 中是 protected 访问控制
// 如果protected是public 这行代码就不会报错了。
// baseClass.f();
}
}
package com.qww.exec04;
public class BaseClass {
protected void f() {
System.out.println("f()");
}
}
package com.qww.exec04.hello;
import com.qww.exec04.BaseClass;
public class SubClass extends BaseClass {
public static void main(String[] args) {
SubClass subClass = new SubClass();
subClass.f();
}
}
练习5
package com.qww.exev05;
public class A {
public int a;
protected int b;
int c;
private int d;
public void f1() {}
protected void f2() {}
void f3() {}
private void f4() {}
}
package com.qww.exev05;
public class Exec05 {
public static void main(String[] args) {
A a = new A();
System.out.println(a.a);
System.out.println(a.b);
System.out.println(a.c);
// Error:(10, 30) java: d 在 com.qww.exev05.A 中是 private 访问控制
// System.out.println(a.d);
a.f1();
a.f2();
a.f3();
// Error:(15, 10) java: f4() 在 com.qww.exev05.A 中是 private 访问控制
// a.f4();
}
}
同一个包里面,不能访问private修饰的成员。
练习6
6.3 接口和实现
使用权限访问控制,有两个原因:
- 设定客户端程序员可以使用和不可以使用的界限,可以修改哪些客户端不是呀的代码,不用担心会影响客户端的代码。
- 将借口和具体实现进行分离。客户端如果只是仅仅向接口发送消息的话,那就可以随意修改所有非public的代码,而不会影响客户端的代码。
6.4 类的访问权限
练习7
package com.qww.exec07;
import com.qww.exec07.access.Widget;
public class Exec07 {
public static void main(String[] args) {
Widget widget = new Widget();
}
}
package com.qww.exec07.access;
public class Widget {
}
类不可以是private
的和protected
的,只有两个选择:包访问权限或者public
.不希望别问对访问该类,可以将该类的每个构造器都改为private
的,这样就可以阻止其他人创建该类的对象。一个例外是,可以在该类的static成员内部创建对象。
特例:内部类可以是private
或者protected
的。
package com.qww.test05;
public class Lunch {
public static void main(String[] args) {
// Error:(6, 23) java: Soup1() 在 com.qww.test05.Soup1 中是 private 访问控制
// Soup1 soup1 = new Soup1();
Soup1 soup1 = Soup1.makeSoup();
Soup2.access().f1();
}
}
class Soup1 {
private Soup1() {}
public static Soup1 makeSoup() {
return new Soup1();
}
}
class Soup2 {
private Soup2() {}
private static Soup2 ps1 = new Soup2();
public static Soup2 access() {
return ps1;
}
public void f1() {}
}
构造器私有化,这样别人就无法创建该类的对象了。该怎么使用这个类的对象呢?有两中做法:
- 在Soup1中,创建一个static方法,这个静态方法创建了一个新的Soup1对象并且返回了它的引用。我们可以在返回这个引用之前做一下额外的工作,比如记录创建了多个对象。
- 使用单例(singleton)设计模式,只能创建一个对象。Soup类的构造器私有化,通过ptivate static的字段来创建对象,所有有且只有一个对象,并且只能通过public的access方法访问它。
练习8
package com.qww.exec08;
public class ConnectionManager {
private static int initCapacity = 6;
private static Connection[] conns = new Connection[initCapacity];
private static int size;
private static ConnectionManager manager = new ConnectionManager();
private ConnectionManager() {}
public static ConnectionManager access() {
return manager;
}
public static Connection[] getConns() {
return size==0 ? null : conns;
}
public Connection getConnection() {
Connection conn = Connection.getInstance();
// 数组扩容两倍
if (size > initCapacity >> 1) copyArr();
conns[size++] = conn;
return conn;
}
private void copyArr() {
Connection[] tmp = new Connection[initCapacity << 1];
for (int i = 0; i < size; i++) {
tmp[i] = conns[i];
}
conns = tmp;
}
public int size() {
return size;
}
}
package com.qww.exec08;
public class Exec08 {
public static void main(String[] args) {
ConnectionManager manager = ConnectionManager.access();
Connection conn = manager.getConnection();
Connection conn2 = manager.getConnection();
Connection conn3 = manager.getConnection();
Connection conn4 = manager.getConnection();
Connection conn5 = manager.getConnection();
Connection[] conns = ConnectionManager.getConns();
if (conns != null) {
for (Connection connection : conns) {
System.out.println(connection);
}
}
}
}
package com.qww.exec08;
public class Connection {
private Connection() {}
public static Connection getInstance() {
return new Connection();
}
}
练习9
package com.qww.exec09.access.foreign;
import com.qww.exec09.access.local.*;
public class Foreign {
public static void main(String[] args) {
// Error:(12, 9)
// java: com.qww.exec09.access.local.PackagedClass在com.qww.exec09.access.local中不是公共的; 无法从外部程序包中对其进行访问
// PackagedClass pc = new PackagedClass();
}
}
package com.qww.exec09.access.local;
class PackagedClass {
public PackagedClass() {
System.out.println("Creating a packaged class");
}
}
PackagedClass
类没有权限访问修饰符修饰,说明是默认权限修饰,包访问权限。位于foreign
包中的Foreign
类是没有权限访问不同包(local
)中的PackagedClass
的。将两个类放到同一个包中,可以解决问题,编译器不会报错。