天天看点

《JAVA编程思想》第四版学习 需要我记住的something –复用类

一.类复用的两种方式:

      组合(composition)--新类由已有类的对象组成。复用已有类的功能,而不是形式。

      继承(inheritance)--以已有类的类型(type)创建新类。无需改变已有类,只是采用新类的形式并添加所需代码。

二.toString()方法的显式、隐式调用(对象作为String来使用时)。

三.null引用访问时报异常,但打印时不受影响。

四.lazy initialization:在用到时才初始化。

五.继承:显式继承其它类,隐式继承Object类。extends关键字

六.为了测试类,可以在每个类中添加一个main方法,而且完成类时也不需要删除。

      任何类(public、package access)的public main方法都可以调用,不论是通过命令行java+类名,还是显式的调用classname.main()。

七.为了继承,一般原则:所有fields设为private,methods为public。(只是个基本的思想问题,protected、private当然也可以用)。

八.super关键字,用来访问父类。

九.派生类对象里包含一个基类的对象(subobject)。

十.Java自动在派生类构造器中插入调用基类构造器的代码,即使未定义派生类构造器,编译器仍会产生一个默认构造器来调用基类构造器,而且基类构造器调用发生在派生类构造器之前。

      那么,是不是可以认为基类构造器调用是派生类构造器的第一条语句,从而在派生类构造器调用的时候才调用呢?不是。请看这个代码:

import static net.mindview.util.Print.*;

class A {

A() { print("A constructor"); }

}

class B {

B() { print("B constructor"); }

}

public class C extends A {

B b = new B();

public static void main(String[] args) {

C c = new C();

}

}

按照初始化顺序,那么定义时的初始化发生在构造器之前,也就是b的赋值应该发生在C的默认构造器之前,如果基类构造器调用是派生类构造器的第一条语句,从而在派生类构造器调用的时候才调用,那运行结果应该是

B constructor

A constructor

而实际结果是

A constructor

B constructor

显然,"基类构造器调用是派生类构造器的第一条语句,从而在派生类构造器调用的时候才调用"这个说法是错误的。应该是:基类的初始化发生在派生类的初始化(包括自动初始化、构造器等等)之前。

     前面说的是无参构造器(默认构造器),对于有参数的构造器:

     1. 如果基类构造器都有参数,必须显式调用基类构造器(super(...))(如果不,编译器报错,找不到基类默认构造器),而且必须是派生类构造器的第一条语句(如果不,编译器报错,找不到基类默认构造器和super调用必须是第一条语句两个错误)。

     2. 如果基类构造器没有参数,则不需显式调用,编译器会为你完成。

     3. 如果派生类有多个构造器,在基类构造器有参数的时候,你显然得在所有派生类构造器中加入super(...);如果基类构造器没有参数,编译器会在所有派生类构造器内加入对基类构造器的调用(因为编译器不知道你会调用哪个构造器,对吧?嘿嘿)。 看例子

import static net.mindview.util.Print.*;

class A {

A() { print("A constructor"); }

}

class B {

B(int i) { print("B constructor"); }

}

public class C7 extends A {

B b = new B(1);

public C7(int i)

{

// super(i);

b = new B(i);

print("C(int i) constructor");

}

public C7()

{

print("C() constructor");

}

public static void main(String[] args) {

C7 c = new C7(10);

c = new C7();

}

}

结果:

A constructor

B constructor

B constructor

C(int i) constructor

A constructor

B constructor

C() constructor

      在显式调用的情况下,似乎上面的讨论又是错的,基类构造器调用就是派生类构造器的第一条语句,在派生类构造器调用的时候才调用。结果呢?再次证明这句话是错的。你仍然得说:基类的初始化发生在派生类的初始化(包括自动初始化、构造器等等)之前 。看这个例子:

import static net.mindview.util.Print.*;

class A {

A(int i) { print("A constructor"); }

}

class B {

B(int i) { print("B constructor"); }

}

public class C7 extends A {

B b = new B(1);

public C7(int i)

{

super(i);

b = new B(i);

print("C constructor");

}

public static void main(String[] args) {

C7 c = new C7(10);

}

}

A constructor

B constructor

B constructor

C constructor

结果说明了一切,虽然你super(i)是在派生类构造器的第一条语句。

      总之一句话,未显式调用基类构造器的情况下,编译器就查找基类的默认构造器,找不到报错;找到,加入对它的调用。基类没有默认构造器,就必须显式调用。基类的初始化发生在派生类的初始化(包括自动初始化、构造器等等)之前。

十一. 委托(delegation):Java不直接支持。

       delegation介于组合和继承之间,新类包含一个成员对象(composition),但同时又在新类中公开该对象的所有(或部分)方法(象继承)。用于处理不能简单的用"has-a"、"is-a"或"is-like-a"来描述的类相互关系的问题(如太空船不是"is-a"或"is-like-a"太空船控制器,但又必须能够进行前进、后退等控制器的操作)。

       可以选择提供部分对象成员的方法,使得应用代理更灵活。

java语言并不支持这种机制,但IDE通常都支持(如JetBrains Idea IDE等)。

         delegation和继承的不同 ,参见DetergentDelegation和Detergent两个类(练习11和书中的源代码):方法执行方式不同(继承通过调用继承父类的方法(不需显式调用),delegation调用自己的方法,其中是对对象成员的方法调用),结果不同。

十二. 编译器并不监督对成员对象的初始化,因此要特别注意。

十三. Java没有析构器,因为我们习惯忘记对象而不是销毁对象。

十四. 确保正确清除。

       一般情况,垃圾回收器就够了。

       如果必须及时清理某些资源,就必须写一个特殊的方法,而且应当让用户知道必须调用该方法。

       首要任务是必须把清除动作置于finally语句中,预防异常的发生?

       清理应按照与初始化相反的次序进行,因为一个子对象可能依赖于另一个,例如继承类可能依赖于基类的对象(继承析构可能要用到基类的对象)。基本假设是后创建的依靠先创建的,所以要先释放后创建的。 (比如你创建了一个按钮,放到一个panel上,panel先创建,按钮后创建,释放时肯定先释放按钮,后释放panel)。

十五. try and finally:不管怎么退出try语句(正常或异常),finally的语句始终会执行。

十六. 垃圾回收器可能以任何顺序回收对象,有时需要按照顺序,所以不能依赖。(还有一个原因,就是垃圾回收器不知道什么时候工作,而且可能永远不会被调用。)

十七. 重载可以跨越基类和派生类,在派生类定义的重载方法不会隐藏任何基类的同名方法(与C++不同,C++会隐藏);当然可以在派生类定义与基类方法签名完全相同(包括返回值)的方法实现重写(override)。这又常常会令人迷惑不解(这也是C++不允许这么做的原因)。

      Java SE5引入了@override注释,不是关键字,用来说明你的方法是override而不是overload,这样编译器在你的实现是overload(而不是你需要的override)时,可以给出错误信息。

十八. 组合和继承的选择

      最根本的:"has-a"和"is-a"。如Car is a vehicle,继承;Car has a Engine,组合。

      组合用于新类需要已有类的功能,而不是其接口时。即新类借助已有类对象实现某种功能,但用户看到的接口是新类的接口,而不是所包含对象的接口。(例如,可能需要某种数据结构List、Set等来存储对象,借助它们的特性实现类的功能,但类的接口并不是它们的接口。)

     一般情况,组合中对象应该为private。 但也有少数情况,直接访问类成员对象更好,此时可为public(所包含对象本身实现也是隐藏的,所以没有安全问题)。这样有个好处是可以帮助客户端程序员理解接口,使类更易用,而且对类的创造者来说,减少了所需的代码复杂度(如Car的例子,参见例Car)。但绝大多数情况还是应该为private。

      虽然继承对OOP来说很重要,当使用更多的是组合而不是继承。应当保持谨慎的态度,只有在确信需要继承的时候才使用。最清晰的办法就是仔细考虑,"是不是真的需要upcasting?"如果是真的,就用继承。这也是选择使用组合还是继承的一个好办法。

十九. 最好的方法是让数据成员(fields)为private(而不是protected),而通过protected methods来允许类继承者访问。

二十. upcasting:安全,继承类是基类的超集,至少包含基类的方法。upcasting只会丢失方法,而不是获取方法(转型之后对所有方法的调用都是安全的)。downcasting则需要类型检查。

二十一. final关键字:这是无法改变的。两个理由:设计或效率。

二十三. final数据:常量-永恒不变的编译时常量(compile-time constant)和不希望被改变的运行时初始化的数据。

      编译时常量,必须是基本类型 ,而且用final定义(public static final定义更常用) ,只能在定义时赋值(只有在定义时赋值的才是编译期常量),大写,下划线分隔。 编译器可把它用于任何用到它的表达式中,表达式可以在编译时计算,减少运行期负担。编译期常量可能会因为优化而消失,而且编译器处理所有编译期常量没有太大区别(编译器只是简单的用常量代替用到它的地方)。但运行时初始化的常量是有区别的, 例如static和non-static的区别。

      final对象,引用是常量,不可变(即初始化后不可再指向另一个对象),但引用所指的对象内容是可变的。注意,Java未提供使任何对象永恒不变的途径(你可以自己实现这样的类),包括数组(即不能定义常量数组)。final引用并没有final基本类型用处大(有用final引用的必要么?它所指向的对象内容是可变的)。

      static final常量,大写,下划线分隔。

      final定义,并不代表其值是编译期就决定的。

      关于static final和final定义的编译期常量,得多说两句。static不用new对象就可以用,而non-static则必须要新建一个对象之后才可以,这是必须的。查询byte code后,发现static final定义的,并不在类中加入相应的field,在用到它的地方用常量代替;而final定义的,在类中是有field,虽然用到它的地方仍然是用常量代替(还有就是可以blank final,见后面)。 这个代码

class AmazingCls {

final static double c = Math.random(); //(1)

//static int a = 18; //(2)

final static int a =18; //(3)

final int b = 19;

static{

System.out.println("-----static---------");

}

{

System.out.println("-----instance-------");

}

public AmazingCls(){

System.out.println("------对象被创建---");

}

public static void main(String[] args){

// int b = AmazingCls.a;

System.out.println("------main()--------");

}

}

其byte code反汇编

static final double c;

static final int a;

final int b;

public AmazingCls();

Code:

0: aload_0

1: invokespecial #1; //Method java/lang/Object."<init>":()V

4: aload_0

5: bipush 19

7: putfield #2; //Field b:I

10: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;

13: ldc #4; //String -----instance-------

15: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

18: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;

21: ldc #6; //String ------对象被创建---

23: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

26: return

public static void main(java.lang.String[]);

Code:

0: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #7; //String ------main()--------

5: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

static {};

Code:

0: invokestatic #8; //Method java/lang/Math.random:()D

3: putstatic #9; //Field c:D

6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;

9: ldc #10; //String -----static---------

11: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

14: return

其中a没有了。。。

二十四. 空白final(blank finals):final定义,但未赋初始值。必须在使用前初始化(在每个构造器中,编译器会保证-不初始化,报错?)。好处是:每个对象的final field可以不同,但又都不可变。

二十五. final参数:在方法内不能改变final参数引用(不能使它指向新的对象);final基本类型,只读,主要用于传数据给内部类。

二十六. final方法:禁止override,出于设计考虑。另外,final方法效率更高(早期JVM)。

       Java早期实现,final方法调用以内联(inline)的形式进行,消除了方法调用的开销(overhead)。对于大的方法,效果不明显。

       最近的JVM(尤其是HotSpot技术)能自动检测这些情况并进行优化。因此,Java SE5/6中,应该把效率问题交给JVM来处理,而只是在需要禁止override时才使用final方法。

       不要陷入强调提前优化的陷阱,如果程序执行慢,并不一定能通过final来解决。程序加速,请参见http://MindView.net/Books/BetterJava profiling。

      private方法是final方法(隐式),不可override,所以不需要额外加上final。可以在派生类中定义同名的方法,但不是override,只是定义了一个同名的新方法而已。此时,向上转型后,调用该方法会报错,因为是private,即使派生类中的同名方法是public。 对于override方法,加上@Override,可以解决一些问题(如果不允许override,会报错)。

      只有类接口才可以继承(非final)。final public or protected方法,尝试override都会报错(即不可在派生类中定义相同签名的方法,不同签名?OK,of course,重载而已)。

二十七. final类:不可继承,其方法也是隐式final;fields可以为final,也可以不是,不受影响。

二十八. 慎用final禁止继承或者override。无法预知类复用的方式,尤其是通用类。

二十九. 类加载发生在第一次使用的时候(创建对象,对static field或static方法的访问)。这同时也是static初始化的时候,只加载一次。

       注意,类的构造器也是隐式static的。所以可以简单的说:类在第一次对其static成员(field and method)进行访问的时候加载。

三十. 有继承时的初始化顺序(加载派生类时,基类也同时加载):

      1. 父类静态成员初始化、显式静态初始化

      2. 子类静态成员初始化、显式静态初始化

      3. 父类自动初始化、子类自动初始化(简单的对象内存清0)

      4. 父类实例初始化(specifying initialization and instance clause)

      5. 父类构造器

      6. 子类实例初始化(specifying initialization and instance clause)

      7. 子类构造器

三十一. 程序开发式一个增量过程(incremental development)