Lab 4: 封装(Encapsulation)

本次 Lab 时长为 2+ 周,DDL 为 10 月 13 日 23:59,希望大家度过一个愉快的国庆假期。🥳

附件下载(包括问答题和实验报告模板):https://bhpan.buaa.edu.cn/link/AA96D0CA694BB74ABFA070F38B6C527AC6

实验目的

  • 理解封装含义
  • 理解信息隐藏的必要性
  • 掌握访问控制修饰符的使用
    • 私有成员(变量和方法)的理解和使用
    • 共有成员的理解和使用
    • 保护成员的理解和使用
    • 使用不加任何权限修饰符的成员
  • 加深对“类和对象”的理解

注意事项

随着代码量的增加,建议建立一个自己的统一且良好的代码风格,比如命名风格(camelCase、PascalCase 等)、缩进方式(空格数量、switch-case 缩不缩进等)、开闭大括号换不换行等容易引发战争(迫真)的东西,以养成良好的编程习惯。

比如命名规范,可以参考 Oracle 官网的 Naming Conventions

编程题最好为每一个类编写一个完备的测试类,覆盖尽可能多的输入、函数调用、输出,以证明代码正确实现了功能。

如果编程题使用了 package 语句,应当确保提交时目录结构和 package 语句表达的包结构相同。(IDE 很多时候会帮你做。)

编程题在给出了具体需求的情况下,可以根据自己的需要添加额外的方法,合理即可。


1. 初始化(Initialization)

阅读并运行下面这段代码,尝试理解 Java 中初始化的顺序。

>
展开代码收起代码
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class A {
int value;
static A a1 = new A(1);

public A(int i) {
System.out.println("initialize A" + i);
value = i;
}

public A(A a) {
System.out.println("copy from A" + a.value);
value = a.value;
}

static A a2 = new A(2);

}

class B {
A a8;
// A a7 = new A(a6);
A a6 = new A(6);
static A a3 = new A(3);
static A a4;

static {
a4 = new A(4);
}

static A a5 = new A(5);

public B(int i) {
System.out.println("initialize B" + i);
a8 = new A(8);
}

A a7 = new A(a6);
}

public class Initialization {
static B b1 = new B(1);
static B b2;

public static void main(String[] args) {
System.out.println("main begins");
A a9 = new A(9);
b2 = new B(2);
System.out.println("main ends");
}
}

Question 1 至 Question 4 均基于该段代码。

Quesion 1

阅读这段代码,给出程序的输出

Question 2

对于非静态属性,它的初始化方法有两种:

  • 在属性定义处显式初始化(如本例中的 a6
  • 在构造方法或非静态方法中初始化(如本例中的 a8

回答下面两个问题(只考虑非静态属性):

  1. 这段代码能够证明“在属性定义处初始化的属性,比在方法中初始化的属性先被初始化”吗?
  2. 这段代码能够证明“在属性定义处初始化的属性,初始化顺序等同于他们在类定义中出现的顺序”吗?

Question 3

请尝试仿照 Question2 的内容,描述静态属性的初始化方式和实际初始化时的顺序

Question 4

已知 static 属性的初始化、static 块的执行,只在 JVM 进行类加载的时候执行,请回答下面的问题

这段代码能够证明“在类的实例第一次被构造、或类的静态属性和静态方法第一次被访问时,JVM 会执行类加载”吗?如果不能,请尝试修改代码并证明。

题外话

懒加载:Lazy Load,对某资源只在需要时才寻找其存在并初始化;对立面是预加载。

预加载:提前加载好所有资源,等待使用资源的那一刻。

对于一些使用频率较低但初始化开销很大的资源,懒加载可以避免他们给程序的初始化增加过多的负担。

JVM 的类加载是懒加载,只有在程序第一次使用到某个类时才去尝试读取其.class文件。类加载只会进行一次,这一次类加载会完成所有的静态初始化工作。更多内容会在后续课程讲解 RTTI 和反射的时候提到。

例如,在游戏编程中,当某一个类的所有实例都使用同一批贴图文件时,可以将贴图资源声明为 static 属性并直接(或在 static 块)初始化。让类的贴图属性引用这些静态资源,这样就可以避免为每一个对象构造单独的贴图文件导致的内存浪费和时间浪费。


2. 单例模式(Singleton Pattern)

阅读下面这段代码,它实现了经典设计模式之一:单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Singleton 一个最简单的单例模式的实现
*/
public class Singleton {
private static final Singleton uniqueInstance = new Singleton();


private Singleton() {
}

public static Singleton getInstance() {
return uniqueInstance;
}

public void foo() {
System.out.println("Aha!");
}
}

Question 5

其他的外部类可以通过 new Singleton() 来构造一个新的 Singleton 变量吗?

Question 6

本题给出的 Singleton 类的写法被称为单例模式,是因为这个类最多只可能有 1 个实例同时存在。为什么只可能有 1 个?这个唯一的实例在什么时候被构造?

Question 7

请写出任意一种外部类调用 Singleton 类的 foo() 的方法。

题外话

这里的 uniqueInstance 初始化方法不是懒加载(Lazy Load)的,因为 uniqueInstance 在类加载时就被初始化了,虽然我们可能最终并用不到它。你可以思考一下如何实现一个懒加载的单例模式。(不要求写在问答报告里。)