Thinking in Java 读书总结(三)
前言
TIJ第7,8,9章分别讲的是类的复用,多态和接口,这些特性在Java中不得不说都是特别特别重要的。之前校招面试的时候其实一直心里有个疑问,Java那么多底层特性真的工作上用得着么,不会是面试造火箭什么的吧。但是真正工作之后静下来去学习才发现,要想真正理解Java里的很多表面的语法和特点,必须要结合底层的特性才能真正知其然且知其所以然。长路漫漫,一点点积累!
第七章 复用类
7.1 组合
实例化位置:
- 定义对象的地方;
- 构造器中;
- 惰性初始化;
- 实力初始化。
7.2 继承
单继承
- 导出类会默认调用弗雷的无参构造器,如果父类中没有无参构造器,有两种解决方案:
- 为父类增加无参构造器;
- 在子类构造器中显示调用父类含参构造器。
7.3 代理
代理实际上就是在代理类内部实例化功能类(名字不太准确。。。),并通过代理类来控制功能类接口的暴露。
7.4 使用组合和继承
7.4.2 名称屏蔽
若基类有多个被重载的方法,在导出类中重载该方法并不会屏蔽其在基类中的任何版本。
@Override注解,如果不小心重载而非复写该方法,编译时会出错。
7.7 向上转型
7.7.1原因
向上转型安全。
7.8 final
final意味着无法改变。final使用三种情况:数据,方法和类。
7.8.1 final数据
- 一个永不改变的编译时常量;
- 一个在运行时初始化的值且不希望被改变。
static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段复制,它没有默认值,必须显示赋值,否则javac会报错。可以理解为在编译器吧结果放入了方法区的运行时常量池中。
public final a = 9; 这里的a是编译时常量。
上面两者都是final的编译期常量。
但是final类型的变量不都是编译期常量!比如final a = Random.nextInt(20);或者static final b = Random(20);这里a和b就都是运行期生成的。还有就是a和b不一样的是a会随着实例的生成,每个实例的a值可能都会不一样,但b只会有一份。这是很基础的知识。但是具体为什么会这样的原因是因为static变量在类装载的时候就已经被初始化。
空白final
空白final就是声明了但没有给定初值的域。但是空白final必须要在构造器中初始化。因为编译器要确保final变量在使用前要被初始化。
final参数
很好理解,在方法中不能改变参数引用指向的对象。
还有个作用,就是向匿名内部类中传递参数!这个在下篇博客讲内部类时会总结下!
7.8.2 final方法
- 方法锁定,继承类不可以覆盖该方法。
- 效率?java6以后就不推荐用这种方法来优化了。之前final方法的机制不是普通方法压入栈这种操作方式。
private方法都是隐式的final方法。
覆盖只有在是某类接口的一部分时才会出现,即A extends B, A向上转型成B时可调用相同方法。private方法就不是基类接口的一部分。
7.8.3 final类
不可继承-->所有方法都隐式的final
7.9 类的初始化
- 父类静态代码区和父类静态成员
- 子类静态代码区和子类静态成员
- 父类非静态代码区和普通成员
- 父类构造函数
- 子类非静态代码区和普通成员
- 子类构造函数
这些都是建立在底层JVM类加载的基础之上的。类初始化的部分可以参考 Java类继承关系中的初始化顺序。这些地方非常关键,在解析过程中会涉及到引用解析,并且具体调用类的那个方法会涉及到分派。而分派又分为静态多分派和动态单分派。
第八章 多态
非常关键的一章,也是和JVM的工作机制尤其是分派联系非常紧密的一章。
8.2 静/动态绑定
静态绑定:发生在编译期,java里只有final方法(含private方法),static方法是静态绑定的,这里要说到我的一个疑问,我目前还不是特别清楚绑定和分配这两个概念在java里的区别。静态分配同样发生在编译期,用来进行非虚方法的选择,非虚方法也就是final,static,私有方法和实力构造器。这里查了好多资料,但是还是不是特别了解。如果读者清楚这里,请在评论里给我留言,非常感谢!
8.2.4 "覆盖"私有方法
私有方法不可以被覆盖!甚至不能重载,但是这里不能重载的意思并不是说子类中不能存在同名不同参数方法,这里的意思是就算写了,父类的private方法在子类中仍然不可见。
8.2.5 域与静态方法
这里要提一下,java里类的成员变量是静态绑定的。
class A{
public int a = 1;
}
class B{
public int a = 2;
}
class C{
public static void main(String[] args){
A a = new B();
System.out.println(a.a);//1
}
}
同样的,正如这一节开始所说,静态绑定是在编译期发生的,其中就有static方法,所以static方法同样不具有多态性!静态方法是与类关联的,而不是对象实例。
8.3 构造器与多态
TIJ这里可能是为了让读者容易理解,犯了一个错误。在之前的博客中有提到,书中认为构造器是隐式的static方法,这是不对的。在调用类方法时,所有参数按顺序存放于被调用方法的局部变量区中的连续区域,从局部变量0开始;在调用实例方法时,局部变量0用于存放传入的该方法所属的对象实例(Java语言中的“this”),所有参数从局部变量1开始存放在局部变量区的连续区域中。
如果要非常详细的了解,可以参考 实例构造器是不是静态方法?
8.3.1 构造器调用顺序
这里可以参考7.9节里的步骤,而子类构造器之所以要调用父类构造器是为了检查对象是否被正确的构造,因为导出类只能访问自己的成员,不能访问基类的成员。
8.3.2 构造器内部的多态!!!
在构造器内部调用其他实例方法,若实例方法被覆盖,那么父类中调用的是覆盖后的方法。这样就会带来一些隐藏的问题,因为父类构造器先调用,而子类方法里用到的域还没有被初始化。
可以看下面的例子:
class Glyph {
void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius
);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
这里父类构造器调用的是子类的draw方法,并且此时radius变量此时还没有被初始化,所以会有一行诡异的结果RoundGlyph.draw(), radius = 0
。
8.4 协变返回类型
用一句很绕口的话描述就是Java导出类的覆写方法可以返回基类被覆写方法返回类的导出类。。。如果一遍没有理解,多想几遍就清楚了。下面给一个代码的例子。
class Grain {
public String toString() { return "Grain"; }
}
class Wheat extends Grain {
public String toString() { return "Wheat"; }
}
class Mill {
Grain process() { return new Grain(); }
}
class WheatMill extends Mill {
Wheat process() { return new Wheat(); }
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
} /* Output:
Grain
Wheat
*///:~
这样应该会清楚很多。
8.5 继承、组合
用继承表达行为间的差异,并用字段表达状态的变化。
纯继承与扩展继承各有优劣,纯继承过于不灵活,但是扩展继承在向上转型的时候会丢失信息,在向下转型的时候会需要类型检查。但是我们现实工作中基本都是扩展继承,我感觉灵活性某种程度上也是继承的生命力所在。
第9章 接口
9.1 抽象类
抽象方法:仅有声明而没有方法体的方法。
abstract void f();
包含抽象方法的类就叫做抽象类。且一个方法如果含有抽象方法,那么必须被限定为抽象的。
但是抽象类可以有实现过的方法!
9.2 接口
接口的域默认是public static final的。
接口的方法必须是public的。
接口对于代码的解耦很有帮助,这个很好理解,因为java里是单继承的,如果想对多个类使用同一个方法,那么他们之间必须有继承关系才行,这样就会有耦合,而接口的出现使得他们之间可以没有任何直接的关系。
策略期模式:简单描述策略器模式和工厂模式很类似,但是前者是类实现同一个接口中的方法,利用方法返回值的不同在第三方类中返回生成不同的类,然后基于生成不同类的实例执行相应的方法。
具体可以看 策略模式和 设计模式学习之策略模式后面这一篇要更加深入一些,前面的更简单易懂。