1. 1. 一切都是对象
    1. 1.1. 1.基本类型与对象的作用域区别?
  2. 2. 初始化与清理
    1. 2.1. 2.为什么不能用返回值区分重载方法?
    2. 2.2. 3.如果你已经构造了构造器,编译器不会再自动帮你创建一个默认构造器(无参构造器)了
    3. 2.3. 4.对象的初始化顺序?
    4. 2.4. 5.回收机制
    5. 2.5. 6.为什么一般不要用finalize,什么时候会用到finalize?
  3. 3. 复用类
    1. 3.1. 7.类的代码在初次使用时才会加载
    2. 3.2. 8.组合与继承,尽管在教授oop的过程中我们多次强调继承,但这并不意味着要尽可能使用它
    3. 3.3. 9.final的三种情况:数据、方法和类。
  4. 4. 多态
    1. 4.1. 10.缺陷:域与静态方法,域的访问并非多态,这个访问在编译期就进行解析。静态方法也无法多态。
    2. 4.2. 11.初始化过程补充
    3. 4.3. 12.向下转型的安全保障由构建系统给出
  5. 5. 接口
    1. 5.1. 13.组合接口名字冲突陷阱
    2. 5.2. 14.注意在接口中的域中,空final不能用了
    3. 5.3. 16.关于优先选择类而不是接口
  6. 6. 内部类
    1. 6.1. 17.内部类最初看来是一种代码隐藏机制,实际上是为了语言的完备性而设计
    2. 6.2. 18.内部类对象一被创建就暗地里链接到了创建它的外部类对象(从而访问外部类的域和方法)
    3. 6.3. 19.内部类可用来完全屏蔽接口的实现细节,完全阻止任何依赖于类型的编码
    4. 6.4. 20.静态内部类(嵌套类)几乎相当于一个独立的类,可以被独立地实例化,但是失去了与外部类的链接
    5. 6.5. 21.接口内部的类!
    6. 6.6. 22.为什么要使用内部类?什么时候用?
    7. 6.7. 23.不可覆盖、可继承

书籍版本:中文版第4版;
这次读 Thinking In Java ,是为 Java 基础知识查漏补缺,以下笔记不代表书中重点内容(但个人觉得值得关注);
读完本篇大约需要30分钟。


一切都是对象

1.基本类型与对象的作用域区别?
1
2
3
4
{
int x = 12;
String s = new String("a string");
}//End of scope

Java 中变量的作用域由花括号的位置决定。

对于基本类型,比如上面的 x ,一旦超出作用域,其生命周期就完结了,内存被释放。

对于对象,只会释放在作用域内定义的引用。比如上面创建的引用 s 在作用域终点就消失了,但指向的 String 对象仍然占据内存。如果在花括号内有把引用传递出去,String 对象仍是可用的,否则就继续占着内存直至被垃圾回收器回收。

(P24 2.3.1 作用域)

初始化与清理

2.为什么不能用返回值区分重载方法?

假设可以,编译器遇到以下这种情况会怎么样?

1
2
3
4
5
6
7
void howAreYou(){}
int howAreYou(){
return 0;
}
void whenCanILeave(){
howAreYou();
}

一脸懵逼!

事实上我们经常处于这种情况——不关心返回值是什么,只是需要调用这个函数。这时候编译器就识别不出你要调用的到底是哪个了。所以必须依靠参数来标识一个函数。

(P82 5.2.3 以返回值区分重载方法)

3.如果你已经构造了构造器,编译器不会再自动帮你创建一个默认构造器(无参构造器)了

(P83 5.3 默认构造器)

4.对象的初始化顺序?

Java 尽力保证所有变量在使用前都能得到恰当的初始化。基于这样的思想,类的内部变量定义,即使散步于方法定义之外,仍旧会在任何方法(包括构造器)被调用前得到初始化。
明白这点,对象的初始化顺序就很容易理解了:

父类静态变量与代码块-->子类静态变量与代码块-->父类非静态变量与代码块-->父类构造器--> 子类非静态变量与代码块-->子类构造器。

(P94 5.7 构造器初始化)

5.回收机制

引用计数,停止-复制,标记-清扫。待补充…
(P89 5.5.4 垃圾回收器如何工作)

6.为什么一般不要用finalize,什么时候会用到finalize?

仔细理解下面👇几条:

finalize 在垃圾回收时刻提供一个清理占用的资源(特殊内存)的机会;
垃圾回收只与内存有关;
所有对象占据的内存最后都由垃圾回收器负责释放;
Java 中一切都是对象。

既然一切都是对象,那这种特殊情况是什么鬼?
这种情况是由于分配内存时采用了类似C语言中的做法,而非Java中的通常做法,一般发生在使用“本地方法”的情况下。比如,在本地 C 方法中,可能调用了 malloc() 函数系列。

所以喽,一般不要用 finalize 的意思是,没有用本地方法你就基本不要用 finalize。

(P87 5.5.1 finalize()的用途何在 )

复用类

7.类的代码在初次使用时才会加载

每个类的编译代码都存在于它自己的独立文件中,该文件只在需要使用程序代码时才会被加载。
这个原理的常见应用是结合静态内部类实现延迟加载的单例模式:

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

是不是很妙?

(P146 7.9 初始化及类的加载)

8.组合与继承,尽管在教授oop的过程中我们多次强调继承,但这并不意味着要尽可能使用它

在开始一个设计时,一般应优先选择使用组合(或者可能是代理),只有在确实必要时才使用继承。因为组合更具灵活性。
目标是找到或创建某些类,其中每个类都有具体的用途,而既不会太大(包含太多功能而难以复用),也不会太小(不添加其他功能就无法使用)。如果你的设计变得过于复杂,通过现有类拆分为更小的部分而添加更多的对象,通常会有所帮助。

(P140 7.7.2 再论组合与继承)

9.final的三种情况:数据、方法和类。

final 数据:告知编译器这块数据是恒定不变的。final 修饰基本类型时很好理解,修饰对象的时候又怎么样呢?效果是使得引用不可变,即无法指向另一个对象,但对于正在指向的对象自身的内容是可以改变的。

final 方法:用来锁定方法,确保方法不会被覆盖。在早期 final 方法还可用来提升效率,但现在已经不提倡了。所以,只有在想明确禁止覆盖的时候,才将方法设置为 final。
private 方法默认隐式指定为final, 无法被继承;如果子类定义相同名称和参数的方法等同于生成了新的方法,没有覆盖父类设置了final的方法。

final 类:类不可继承。

待补充…有关final的忠告。

(P140 7.8 final 关键字)

多态

10.缺陷:域与静态方法,域的访问并非多态,这个访问在编译期就进行解析。静态方法也无法多态。

(P156 8.2.5 缺陷:域与静态方法)

11.初始化过程补充

1)在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的0
2)基类构造器
3)成员、代码块初始化
4)导出类的构造器
运行异常时看看其他东西是否都是0,这个线索可以判断异常的原因是否因为某些需要初始化的东西忘记初始化了。

记住一条准则:“用尽量简单的方法使对象进入正常状态,可以的话,避免调用其他方法”;事实上,构造器内唯一安全的调用方法是父类中的final方法。这条准则很难说总是遵循,但应该朝着它努力。

(p157 8.3 构造器和多态)

12.向下转型的安全保障由构建系统给出

就是抛出运行时异常。

(P167 8.5.2 向下转型与运行时类型识别)

接口

13.组合接口名字冲突陷阱

implements 两个接口, 两个接口中定义了一个一摸一样的方法,会怎么样?
出错。只能implements 其中一个,另一个利用内部类实现。

(P181 9.5.1 组合接口时的名字冲突)

14.注意在接口中的域中,空final不能用了

(P184 9.7.1 初始化接口中的域)

#####15.接口的域值,存储在静态存储区
(P184 9.7.1 初始化接口中的域)

16.关于优先选择类而不是接口

“确定接口是理想选择,因而应该总是选择接口而不是具体的类。”这其实是一种引诱。
许多人都掉进了这种诱惑的陷阱,只要有可能就去创建接口和工厂。这种逻辑看起来好像是因为需要使用不同的具体实现,因此总是应该添加这种抽象性。然而这实际上已经变成一种草率的设计优化。
任何抽象性都应该是应真正的需求而产生的。当必需时,你应该重构接口而不是到处添加额外级别的间接性,并由此带来额外的复杂性。这种额外的复杂性非常显著,尤其是你让某人去处理这种复杂性,仅仅因为你由于以防万一儿添加了新接口。

总之,记住——
任何抽象性都应该是应真正的需求而产生的。
任何抽象性都应该是应真正的需求而产生的。
任何抽象性都应该是应真正的需求而产生的。

重要的事情说三遍。

内部类

17.内部类最初看来是一种代码隐藏机制,实际上是为了语言的完备性而设计

弥补Java无法多重继承的特性和解决接口名字冲突陷进。

(P190 10 内部类)

18.内部类对象一被创建就暗地里链接到了创建它的外部类对象(从而访问外部类的域和方法)

因此,对非静态内部类而言,在拥有外部类对象前不可创建内部类对象。

(P191 10.2 链接到外部类)

19.内部类可用来完全屏蔽接口的实现细节,完全阻止任何依赖于类型的编码
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
/*对于客户端程序员而言,他完全不知道 PContents 的存在,也就不会出现依赖于 PContents 的编码了,只能依赖接口 Contents 。Destination也类似。*/
class Parcel4 {
private class PContents implements Contents {
private int i = 11;

public int value() {
return i;
}
}

protected class PDestination implements Destination {
private String label;

private PDestination(String whereTo) {
label = whereTo;
}

public String readLabel() {
return label;
}
}

public Destination destination(String s) {
return new PDestination(s);
}

public Contents contents() {
return new PContents();
}
}

public class TestParcel {
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
// Illegal -- can't access private class:
//! Parcel4.PContents pc = p.new PContents();
}
} ///:~

(p194 10.4 内部类与向上转型)

20.静态内部类(嵌套类)几乎相当于一个独立的类,可以被独立地实例化,但是失去了与外部类的链接

(P201 10.7 嵌套类)

21.接口内部的类!

这次阅读最恍然大悟的收获——静态内部类可以作为接口的一部分!甚至可以再内部类中实现其外围接口!

1
2
3
4
5
6
7
8
9
public interface ClassInInterface{
void holdTheDoor();
class Test implements ClassInInterface{

@Override public void holdTheDoor() {
System.out.println("Hodor");
}
}
}

如果你想要创建某些公共代码,使得它们可以被某个街口的所有不同实现所共有,那么使用接口内部类显得非常方便。

比如MVP模式的View层接口,经常会定义 一个showProgress() 方法来显示进度条,每个实现类都要写一遍实现方法非常麻烦,这时候接口内部类正好大显身手。

但是要注意你是否确实该采用这种接口内部类的方案来提供公共代码。因为遇到这种既需要定义接口、又要提供公共代码的需求时,说明可能要考虑你这个接口是否应该改为抽象类。通过经典的 isA 和 hasA 关系来判断。
(P202 10.7.1 接口内部的类)

22.为什么要使用内部类?什么时候用?

多继承的完整解决方案。或者需要同一接口的不同的实现处于一个类中。
(P204 10.8 为什么需要内部类)

23.不可覆盖、可继承

如果创建了一个内部类,然后继承并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?这看起来是很有用的思想,但是“覆盖”内部类酒好像是外围类的一个方法,其实并不起什么作用。这两个内部类是完全独立的两个实体,各自在自己的命名空间里。 当然,明确地继承某个内部类也是可以的。
(P212 10.10 内部类可以被覆盖吗)