CS61B 课程笔记(Lecture 15 Packages, Access Control, Objects)
CS61B 课程笔记(Lecture 15 Packages, Access Control, Objects)
包和 JAR 文件
概念
在大型项目中,可能会出现类名冲突的问题。例如,如果你定义了一个
Dog
类,而其他项目中也有一个同名的 Dog
类,你的代码将如何区分这两个类呢?为了解决这个问题,我们使用包(Package)来组织类和接口,以创建一个命名空间。
命名约定
通常,包名应以网站地址的反向书写为开头。例如,Josh Hug 的动物包可以命名为:
1 | ug.joshh.animal; // Josh Hug 的网站是 joshh.ug |
在 CS61B 中,尽管不需要遵循这个约定,但了解它是有益的。
使用包
- 同一包内的类:可以直接使用类的简单名称。例如:
1 | Dog d = new Dog(...); |
- 跨包访问:需要使用完整的规范名称,例如:
1 | ug.joshh.animal.Dog d = new ug.joshh.animal.Dog(...); |
- 导入包:为了简化代码,可以使用
import
语句。例如:
1 | import ug.joshh.animal.Dog; |
创建包
创建包的步骤:
- 在每个文件顶部添加包名:
1 | package ug.joshh.animal; |
- 将文件存储在与包名相匹配的文件夹中。即
ug.joshh.animal
应存放在ug/joshh/animal
文件夹内。
在 IntelliJ 中创建包
- 点击文件 → 新建包。
- 输入包名(例如:“ug.joshh.animal”)。
向包添加 Java 文件
- 新文件:
- 右键单击包名。
- 选择新建 → Java 类。
- IntelliJ 将自动将类放入正确的文件夹,并添加“package ug.joshh.animal”声明。
- 旧文件:
- 在文件顶部添加“package [packagename]”。
- 将 .java 文件移动到相应的文件夹。
默认包
任何没有显式包名的类将自动属于默认包。尽量避免将文件留在默认包中,因为这样做会导致访问限制和命名冲突的问题。
示例
如果 DogLauncher.java
在默认包中,则不能在默认包外访问:
1 | DogLauncher.launch(); // 不会工作 |
JAR 文件
JAR 文件是将多个 .class 文件“打包”在一起的单个文件,便于分享程序。JAR 文件类似于 zip 文件,可以解压缩并获取原始 .java 文件,因此并不能保护代码。
在 IntelliJ 中创建 JAR 文件
- 文件 → 项目结构 → 产物 → JAR → 从带依赖项的模块。
- 点击确认。
- 构建产物(这将创建 JAR 文件)。
- 将 JAR 文件分享给其他程序员。
构建系统
构建系统可以自动化项目设置的过程,尤其在团队协作中非常有用。尽管在 CS61B 中使用的构建系统相对简单,但在项目中(如 Maven)使用构建系统的优势仍然显著。
访问控制
访问控制涉及到类成员的可见性和访问权限。Java 提供了四种访问控制级别:
私有(private):仅该类可访问,子类和同包中的类无法访问。
包私有(default/package-private):没有修饰符时,默认的访问权限,属于同一包的类可以访问,但子类不能。
保护(protected):同一包中的类和子类可以访问,但包外的类不能访问。
公有(public):对所有类开放,通常被外部客户端依赖。
练习 7.1.1
尝试从记忆中绘制访问表,使用以下列标题:修饰符、类、包、子类、世界。行标题为:公有、保护、包私有、私有。
修饰符 | 类 | 包 | 子类 | 世界 |
---|---|---|---|---|
公有 | 可访问 | 可访问 | 可访问 | 可访问 |
保护 | 可访问 | 可访问 | 可访问 | 不可访问 |
包私有 | 可访问 | 可访问 | 不可访问 | 不可访问 |
私有 | 可访问 | 不可访问 | 不可访问 | 不可访问 |
访问控制的微妙之处
默认包的特殊性:没有包声明的类成员仍可在默认包中访问,尽管不推荐这样做。
访问仅基于静态类型:接口方法默认是公有的,访问控制基于静态类型而非动态类型。
练习 7.1.2
对于给定代码,识别 demoAccess
方法中会在编译时出错的行。
1 | package universe; |
代码结构
接口
BlackHole
:
1
2
3
4 package universe;
public interface BlackHole {
void add(Object x); // 这个方法是公有的
}
BlackHole
是一个公有接口,任何类都可以实现这个接口,且add
方法是公有的,可以被任何地方访问。类
CreationUtils
:
1
2
3
4
5
6 package universe;
public class CreationUtils {
public static BlackHole hirsute() {
return new HasHair();
}
}
- 这个类提供了一个公有静态方法
hirsute
,它返回一个HasHair
类型的实例。由于HasHair
类是包私有的,因此只能在universe
包内访问。类
HasHair
:
1
2
3
4
5
6 package universe;
class HasHair implements BlackHole {
Object[] items;
public void add(Object o) { ... }
public Object get(int k) { ... }
}
HasHair
实现了BlackHole
接口,但它是包私有的(没有修饰符)。这意味着只有同一包中的类可以访问HasHair
类。类
Client
:
1
2
3
4
5
6
7
8
9 import static CreationUtils.hirsute;
class Client {
void demoAccess() {
BlackHole b = hirsute(); // 访问 CreationUtils 的静态方法
b.add("horse"); // 正确,因为 add 是公有方法
b.get(0); // 出错,b 的静态类型是 BlackHole,未定义 get 方法
HasHair hb = (HasHair) b; // 出错,HasHair 是包私有的,Client 类无法访问
}
}
- 在
Client
类中,我们导入了hirsute
方法并调用它,得到一个BlackHole
类型的引用b
,指向HasHair
的实例。错误分析
b.get(0);
:
- 这里出错的原因是,
b
的静态类型是BlackHole
,而BlackHole
接口并没有定义get
方法。尽管b
动态上是HasHair
类型,但编译器在检查时只考虑静态类型,因此无法识别get
方法,导致编译错误。HasHair hb = (HasHair) b;
:
- 这行代码尝试将
b
转换为HasHair
类型。虽然在universe
包内,b
确实是HasHair
的实例,但Client
类位于包外,因此无法访问包私有的HasHair
类。这也导致编译错误。