《Thinking In Java第四版》拾遗
转自我的github (http://katsurakkkk.github.io/2016/05/Thinking-In-Java%E7%AC%AC%E5%9B%9B%E7%89%88-%E6%8B%BE%E9%81%97 )
近日重读了《Thinking In Java第四版》(可能版本比较老),发现一些有趣的和值得注意的地方,在此作个记录。
- 返回值过载
不能用返回值对函数进行overload,因为有可能调用方并不关心返回值,这就造成了二义性。如:
1 void f(); 2 int f();
调用f()的时候就会造成二义性。
- this引用
可以用this引用在构造函数中调用过载的构造函数,但是只能调用一个,并且构造函数调用必须是我们做的第一件事,否则编译器会报错。另外,只能在构造函数中调用其它构造函数。
1 public class ThisTest { 2 public ThisTest() { 3 // System.out.print("Constructor"); 4 this(1); 5 // this(1, 2); 6 } 7 public ThisTest(int n) {} 8 9 public ThisTest(int n, int m) {} 10 }
- 属性初始化
可以使用方法调用对属性进行初始化,但是不能调用未初始化的属性。注意,属性的初始化与代码顺序有关,而不是与程序的编译方式有关。
1 class PropertyInitTest { 2 int i = init(); 3 int j = init(i); 4 5 // Compile Error Here. 6 // int n = init(m); 7 // int m = init(); 8 9 public int init() { 10 return 1; 11 } 12 13 public int init(int n) { 14 return n + 1; 15 } 16 }
- 非静态属性的代码块初始化方式
匿名内部类的属性初始化必须使用这种方式。
1 public class ThisTest { 2 public static void main(String[] args) { 3 InitCodeTest ict = new InitCodeTest(); 4 ict.printProperties(); 5 } 6 } 7 8 class InitCodeTest { 9 int i; 10 int j; 11 12 public InitCodeTest() { 13 System.out.println("Constructor."); 14 } 15 16 { 17 i = increase(1); 18 j = increase(2); 19 } 20 21 int increase(int n) { 22 System.out.println("increase:" + n); 23 return n + 1; 24 } 25 26 void printProperties() { 27 System.out.println("i=" + i + "|j=" + j); 28 } 29 }
代码运行的输出如下:
1 increase:1 2 increase:2 3 Constructor. 4 i=2|j=3
- Compilation unit
每个Compilation unit就是一个java文件,必须以一个.java结尾,而且在编辑单元内部可以有一个public类,必须与编辑单元的名字相同。并且每个编辑单元中只能有一个public类,但是可以有多个非public类。如下:
1 // EditUnit.java 2 public class EditUnit { 3 } 4 5 class NotPublic { 6 }
- final方法
1. 不希望方法在继承过程中改变
2. inline,将一个方法设置成final以后,编译器就可以把对那个方法的所有调用都变成内联函数,直接加入需要调用的地方,免除了函数调用的入栈和退栈操作。但是过大的方法内联会使得程序变得臃肿,所以不要过于相信编译器的判断,最好只有在方法代码量非常少或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
3. 类内所有private方法都自动成为final。可以为一个private方法添加final提示符,但却不能为那个方法提供任何额外的含义。
- 接口中定义的字段
接口中定义的字段会自动具有static和final属性。它们不能是‘空白final’,但可以初始化成非常数表达式。如下:
1 public interface RandVals { 2 int rint = (int)(Math.random() * 10); 3 long rlong = (long)(Math.random() * 10); 4 float rfloat = (float)(Math.random() * 10); 5 double rdouble = (double)(Math.random() * 10); 6 }
由于字段是static的,所以会在首次装载类之后,以及首次访问任何字段之前获得初始化。所以接口里面的属性可以理解为一个static字段,它们也是保存于那个接口的static存储区域中。
- 内部类和upcast
内部类看起来没有什么特别的地方,然而当我们准备upcast到一个基类的时候,内部类就开始发挥其关键作用。这是由于内部类随后可以完全进入不可见或者不可用的状态--对任何人都如此。所以我们可以非常方便的隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。如下:
1 abstract class Contents { 2 abstract public int value(); 3 } 4 5 interface Destination { 6 String readLabel(); 7 } 8 9 class Parcel { 10 private class PContents extends Contents { 11 private int i = 11; 12 13 @Override 14 public int value() { 15 return i; 16 } 17 } 18 19 protected class PDestination implements Destination { 20 private String label; 21 22 private PDestination(String whereTo) { 23 label = whereTo; 24 } 25 26 @Override 27 public String readLabel() { 28 return label; 29 } 30 } 31 32 public Destination dest(String s) { 33 return new PDestination(s); 34 } 35 36 public Contents cont() { 37 return new PContents(); 38 } 39 } 40 41 class Test { 42 public static void main(String[] args) { 43 Parcel p = new Parcel(); 44 Contents c = p.cont(); 45 Destination d = p.dest("ShenZhen"); 46 // Illegal -- can't access private class: 47 //! Parcel.PContents c = p.new Pcontents(); 48 } 49 }
Contents和Destination代表可由程序员调用的接口,在Parcel中,内部类PContents设为private,所以除了Parcel之外,其他东西不能访问它。PDestination设为protected,所以除了Parcel,Parcel包内的类,以及Paarcel的继承者外,其他东西都不能访问PDestination。事实上,我们不能downcast到一个private内部类(或者一个protected内部类,除非自己本身是一个继承者),因为我们不能访问名字。所以,利用private内部类,类设计人员可以完全禁止其他人以来类型编码,并可以将具体的实施细节完全隐藏。除此之外,从客户程序员的角度看,一个接口的范围没有意义的,因为他们不能访问不属于公共接口类的任何额外方法。这样一来Java编译器也有机会生成效率更高的代码。
另外,普通(非内部)类不能设置为private或者protected--只允许public或者package。
- 匿名内部类
看下面这段代码
1 public class InnerClass { 2 public Contents count() { 3 return new Contents() { 4 private int i = 11; 5 public int value() { 6 return i; 7 } 8 } 9 } 10 }
cont()方法同时合并了返回值的创建代码,以及用于那个返回值的类。除此之外,这个类是匿名的。这种语法要表达的意思是:“创建从Contents衍生出来的一个匿名类对象”。由new表达式返回的实例引用会自动upcast成一个Contents引用。匿名内部类的语法其实要表达的是:
1 class MyContents extends Contents { 2 private int i = 11; 3 public int value() { 4 return i; 5 } 6 return new MyContents(); 7 }
如果需要用到基类的带有参数的构造函数,那么可以在new的时候使用带有参数的构造函数。如下:
1 public Contents count(int x) { 2 return new Contents(x) { 3 private int i = 11; 4 public int value() { 5 return i; 6 } 7 } 8 }
注意,匿名类不能拥有一个构造函数,这和在构造函数中调用super()的常规做法不同。
在匿名内部类中,如果需要使用外部对象则需要该外部对象为final属性。如果需要采取一些类似于构造函数的行为,可以通过前面提到的初始化代码块实现。如下:
1 public Contents count(final Integer x) { 2 private int cost; 3 { 4 cost = (int)(Math.random() * 10); 5 System.out.println(cost); 6 } 7 int i = x; 8 public value() { 9 return i; 10 } 11 }
- 从内部类继承
由于内部类构建起必须同封装对象的一个引用联系到一起,所以从一个内部类集成的时候,情况会变得比较独特。问题在于封装类的隐含引用必须获得初始化,而在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联:
1 class WithInner { 2 class Inner {} 3 } 4 5 class InheritInner extends WithInner.Inner { 6 // InheritInner() {} // Compile error. 7 InheritInner(WithInner wi) { 8 wi.super(); 9 } 10 }
可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但是在需要创建一个构造函数的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个句柄。此外,必须在构造函数中采用下述语法:
1 enclosingClassHandle.super();
它提供了必要的句柄,以便程序正确编译。
- 继承和finalize()
在继承的时候必须OverWrite衍生类中的finalize()方法,在覆盖衍生类的finalize()方法时,务必记住调用finalize()的基础版本。否则,基础类的初始化根本不会发生。finalize()的顺序最好和类的初始化顺序相反,这是由于衍生类finalize()的时候可能会要求基类的组件仍然处于活动状态。
- 类初始化过程
1. 为对象分配的存储空间初始化成二进制零。
2. 初始化基类
3. 按照声明的顺序调用成员初始化代码
4. 调用衍生类的构造函数
关于类的初始化过程最好去看一下《Java虚拟机规范》--https://docs.oracle.com/javase/specs/
- 继承中的异常声明
这个分为两种情况,一种是对于普通方法OverWrite的,另一种是对于构造函数的。
1. 对于普通方法声明的异常范围只能在继承和覆盖时变得更“窄”。也就是说在继承和覆盖的时候方法声明的异常不能超过父类中方法声明的异常的范围。因为在upcast的时候调用接口会多态调用到子类中的方法,如果子类覆盖的方法声明的异常超过父类接口,那么调用的代码将无法处理意想不到的异常。
2. 对于构造函数,子类构造函数中声明的异常只能变得更宽。也就是说子类的构造函数必须至少声明父类构造函数中声明的异常。这也很好理解,如果不是这样的话那么在子类构造实例的时候会调用父类的构造函数,这样调用者也有可能会收到意想不到的异常。
- 关于异常的一些事项
若调用了break和continue语句,finally语句也会得以执行。与break和continue一道,finally排除来了Java对跳转语句的需求。
关于异常的准则(用异常做下列事情):
1. 解决问题并再次调用造成异常的方法。
2. 平息事态的发展,并在不重新尝试方法的前提下继续。
3. 计算另一些结果,而不是希望方法产生的结果。
4. 在当前环境中尽可能的解决问题,以及将相同的异常重新抛出一个更高级的环境。
5. 在当前环境中尽可能的解决问题,以及将不同的威力重新抛出一个更高级的环境。
6. 终止程序执行。
7. 简化编码。若异常方案使事情变得更加复杂,那就会使人非常烦恼,不如不用。
8. 使自己的库和程序变得更加安全。这是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性“
- 关于序列化
自定义序列化:
1. 实现Externalizable接口,实现writeExternal()和readExternal()方法。若从一个Externalizable对象继承,通常要调用writeExternal()和readExternal()方法的父类版本,以便正确的保存和回复基础类组件。
2. 实现Serializeble接口,并添加(注意是添加,不是实现或者覆写)writeObject()和readObject()方法。一旦对象被序列化或者重新装配就会调用这两个方法。也就是说,只要提供了这两个方法,就会优先使用它们而不考虑默认的序列化机制。两个方法的声明如下:
1 private void writeObject(ObjectOutputStream stream) throws IOException; 2 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
看起来似乎我们调用ObjectOutputStream.writeObject()的时候,我们传递给它的Serializable对象似乎会被检查是否自己实现了writeObject(),如果是就会跳过常规的序列化过程,并调用writeObject()。readObject()也会遇到类似问题。
在我们的writeObject()内部,可以调用defaultWriteObject(),从而决定采取默认的writeObject()行为。类似的在readObject()中,可以调用defaultReadObject()。如下:
1 private void writeObject(ObjectOutputStream stream) throws IOException { 2 stream.defaultWriteObject(); 3 stream.writeObject(/*A transient property, for example.*/); 4 } 5 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 6 stream.defaultReadObject(); 7 b = (String)stream.readObject(); 8 }
如果对象A和对象B共同引用了对象C,那么如果把A和B序列化到一个流里面,那么只有一个对象C会被序列化,但是如果把A和B分别序列化到不同的流,再反序列化出来的时候就会有两个不同的对象C。
对于static的属性,需要我们亲自动手去序列化和反序列化。
- 关于clone
clone()方法是定义在Object类中的,但是可能考虑到不是每个类都需要克隆的能力,所以把Object中的clone方法设置成了protected。如果某个类想要拥有clone的特性,可以在类中对clone进行覆写,将它设置成public,然后再里面调用super.clone()。
除了覆写clone()方法,需要clone特性的类还需要实现Cloneable()接口。虽然这个接口并没有定义任何的方法,只是作为一个标记。
两方面的原因促成了Cloneable interface的这种存在。首先,可能有一个upcast句柄指向一个基础类型,而且不知道它是否真的能克隆那个对象。在这种情况下,可以用instanceof关键字调查句柄是否确实同一个能克隆的对象连接:
1 if (myHandle instanceof Cloneable) //...
第二个原因是考虑到我们可能不愿所有的对象都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable接口。若答案是否定的,则抛出一个CloneNotSupportedException异常。
总之,如果希望一个类能够克隆,那么:
1. 实现Cloneable接口。
2. 覆写clone()。
3. 在自己的clone()中调用super.clone()。
4. 在自己的clone()中捕获异常。
- 为何会堵塞
线程的堵塞可能是由于下述五方面的原因造成的:
1. 调用sleep(m),使线程进入”睡眠“状态。在规定的时间内,这个线程是不会运行的。
2. 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回”可运行“状态。
3. 用wait()暂停了线程的执行。除非线程收到notify()或者notifyAll()消息,否则不会变成”可运行“状态。
4. 线程正在等候一些IO(输入输出)操作完成。
5. 线程试图调用另一个对象的”同步“方法,但是那个对象处于锁定状态,暂时无法使用。
在此推荐《Java并发编程实战》一书。
- wait()和notify()
wait()和notify()比较特别的地方是这个方法属于基础类Object的一部分,不像sleep(),suspend()以及resume()那样属于Thread的一部分。而且,我们能调用wait()的唯一地方是在一个同步的方法或者代码块内部。若在一个不同步的地方调用了wait()或者notifiy(),尽管程序仍然会通过编译,但是在运行的时候会得到一个IllegalMonitorStateException。
另外,最好不要使用stop()方法停止线程,因为它会解除所有由线程获取的锁定,而且如果对象处于一种不连贯状态,那么其他线程在那种状态下检查和修改它们,便会造成数据的不一致。
对于阻塞线程的停止,最好使用Thread提供的interrupt()方法。