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
2
3
import ug.joshh.animal.Dog;
...
Dog d = new Dog(...);

创建包

创建包的步骤:

  1. 在每个文件顶部添加包名:
1
2
3
4
5
6
package ug.joshh.animal;
public class Dog {
private String name;
private String breed;
// ...
}
  1. 将文件存储在与包名相匹配的文件夹中。即 ug.joshh.animal 应存放在 ug/joshh/animal 文件夹内。

在 IntelliJ 中创建包

  1. 点击文件 → 新建包。
  2. 输入包名(例如:“ug.joshh.animal”)。

向包添加 Java 文件

  • 新文件
    1. 右键单击包名。
    2. 选择新建 → Java 类。
    3. IntelliJ 将自动将类放入正确的文件夹,并添加“package ug.joshh.animal”声明。
  • 旧文件
    1. 在文件顶部添加“package [packagename]”。
    2. 将 .java 文件移动到相应的文件夹。

默认包

任何没有显式包名的类将自动属于默认包。尽量避免将文件留在默认包中,因为这样做会导致访问限制和命名冲突的问题。

示例

如果 DogLauncher.java 在默认包中,则不能在默认包外访问:

1
2
DogLauncher.launch(); // 不会工作
default.DogLauncher.launch(); // 不存在

JAR 文件

JAR 文件是将多个 .class 文件“打包”在一起的单个文件,便于分享程序。JAR 文件类似于 zip 文件,可以解压缩并获取原始 .java 文件,因此并不能保护代码。

在 IntelliJ 中创建 JAR 文件
  1. 文件 → 项目结构 → 产物 → JAR → 从带依赖项的模块。
  2. 点击确认。
  3. 构建产物(这将创建 JAR 文件)。
  4. 将 JAR 文件分享给其他程序员。

构建系统

构建系统可以自动化项目设置的过程,尤其在团队协作中非常有用。尽管在 CS61B 中使用的构建系统相对简单,但在项目中(如 Maven)使用构建系统的优势仍然显著。

访问控制

访问控制涉及到类成员的可见性和访问权限。Java 提供了四种访问控制级别:

  • 私有(private):仅该类可访问,子类和同包中的类无法访问。

  • 包私有(default/package-private):没有修饰符时,默认的访问权限,属于同一包的类可以访问,但子类不能。

  • 保护(protected):同一包中的类和子类可以访问,但包外的类不能访问。

  • 公有(public):对所有类开放,通常被外部客户端依赖。

练习 7.1.1

尝试从记忆中绘制访问表,使用以下列标题:修饰符、类、包、子类、世界。行标题为:公有、保护、包私有、私有。

修饰符 子类 世界
公有 可访问 可访问 可访问 可访问
保护 可访问 可访问 可访问 不可访问
包私有 可访问 可访问 不可访问 不可访问
私有 可访问 不可访问 不可访问 不可访问

访问控制的微妙之处

  • 默认包的特殊性:没有包声明的类成员仍可在默认包中访问,尽管不推荐这样做。

  • 访问仅基于静态类型:接口方法默认是公有的,访问控制基于静态类型而非动态类型。

练习 7.1.2

对于给定代码,识别 demoAccess 方法中会在编译时出错的行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package universe;
public interface BlackHole {
void add(Object x); // 这个方法是公有的
}

package universe;
public class CreationUtils {
public static BlackHole hirsute() {
return new HasHair();
}
}

package universe;
class HasHair implements BlackHole {
Object[] items;
public void add(Object o) { ... }
public Object get(int k) { ... }
}

import static CreationUtils.hirsute;
class Client {
void demoAccess() {
BlackHole b = hirsute();
b.add("horse");
b.get(0); // 这里出错,因为 b 的静态类型是 BlackHole
HasHair hb = (HasHair) b; // 这里出错,因为 HasHair 是包私有的
}
}

代码结构

  1. 接口 BlackHole

    1
    2
    3
    4
    package universe;
    public interface BlackHole {
    void add(Object x); // 这个方法是公有的
    }
    • BlackHole 是一个公有接口,任何类都可以实现这个接口,且 add 方法是公有的,可以被任何地方访问。
  2. CreationUtils

    1
    2
    3
    4
    5
    6
    package universe;
    public class CreationUtils {
    public static BlackHole hirsute() {
    return new HasHair();
    }
    }
    • 这个类提供了一个公有静态方法 hirsute,它返回一个 HasHair 类型的实例。由于 HasHair 类是包私有的,因此只能在 universe 包内访问。
  3. 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 类。
  4. 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 的实例。

错误分析

  1. b.get(0);
    • 这里出错的原因是,b 的静态类型是 BlackHole,而 BlackHole 接口并没有定义 get 方法。尽管 b 动态上是 HasHair 类型,但编译器在检查时只考虑静态类型,因此无法识别 get 方法,导致编译错误。
  2. HasHair hb = (HasHair) b;
    • 这行代码尝试将 b 转换为 HasHair 类型。虽然在 universe 包内,b 确实是 HasHair 的实例,但 Client 类位于包外,因此无法访问包私有的 HasHair 类。这也导致编译错误。