× 可以利用别人已经写好、已测试通过的类来创建新的类。有两种方法:合成(composition)和继承(inheritance)。
× 继承类自动从基类获得了所有的成员和方法,但是只能访问基类的一部分成员和方法。
× 不论是否public类,都可以为每一个类创建一个main()方法,这样,测试代码就可以放到类里面。
× 可以在继承类中修改一个在基类中定义的方法,如果在新方法中需要调用基类的方法,不能直接调用方法名,这样会导致递归的发生(还是调用了新类的方法),这时,需要使用super关键字,如:super.f1();
× 基类的初始化:
继承类不仅仅是拷贝了基类的接口,当你创建了一个继承类的对象时,这个对象里还包括了一个基类生成的子对象。
基类会在派生类的构造方法访问它之前完成基类的初始化。
如果基类中没有声明构造方法(此时解释器会生成不带参数的默认构造方法),或者基类中重载的构造方法中有一个不带参数,那么,不论派生类中出现以下那种情况,在派生类实例化的时候会自动调用基类的那个不带参数的构造方法:
1、 派生类中没有声明任何构造方法,于是解释器为派生类也自动创建了一个缺省构造方法;
2、 派生类实例化时调用的构造方法(不论这个派生类构造方法是否带参数)的方法体中没有使用super();语句;
3、 派生类实例化时调用的构造方法(不论这个派生类构造方法是否带参数)的方法体中使用了super();语句(这
时,基类的构造方法还是只被调用一次);
如果基类中声明了构造方法(此时就不存在默认构造方法了),而且基类中重载的构造方法都带有参数,那么在派生类中存在下面两种情况:
1、 派生类中没有声明任何构造方法,或者派生类实例化时被调用的派生类的方法体中没有使用super关键字和
合适的参数来显式地调用基类的构造方法,那么编译器就会报错说它找不到基类的构造方法;
2、 派生类中声明了构造方法,而且在这个派生类的构造方法的方法体中第一行就使用super关键字和合适的参数
显式地调用了基类的某个构造方法(如:super(a,b,c);),那么编译通过;
× 合成用于新类要使用旧类的功能,而不是使用旧类的接口的场合,这样,用户看到的是新类的接口。
使用合成时,让用户直接访问新类中的各个组成部分也是合乎情理的,也就是说,可以将新类的成员定义成public,各个成员也都有自己的“隐藏实现”机制,因此这样做也是安全的,同时也降低了新类的开发难度。但是,这是一个特例,一般情况下,你应该将类成员数据定义成private。
× 继承要表达的是一种“是(is-a)”关系,而合成表达要表达的是“有(has-a)”关系。
× 最好的做法是,将数据成员设成private 的;你应该永远保留修改底层实现的权利。然后用protected 权限的方法来控制继承类的访问权限:
class Villain {
private String name;
protected void set(String nm) { name = nm; }
public Villain(String name) { this.name = name; }
public String toString() {
return "I'm a Villain and my name is " + name;
}
}
× 继承的优点之一就是,它支持渐进式的开发(incremental develop)。添加新的代码的时候,不会给老代码带来bug。
× 继承最重要的特征不在于它为新类提供了方法,而是它表达了新类同基类之间的关系。这种关系可以被归纳为一句话“新类就是一种原有的类。”
把派生类当作基类来使用被称为“上传(upcasting)”。上传总是安全的,因为你是把一个较具体的类型转换成
较为一般的类型。也就是说派生类是基类的超集(superset),它可能会有一些基类所没有的方法,但是它最少要有基类的方法。在上传过程中,类的接口只会减小,不会增大。这就是为什么编译器会允许你不作任何明确的类型转换或特殊表示就进行上传的原因了。
× 在判断该使用合成还是继承的时候,有一个最简单的办法,就是问一下你是不是会把新类上传给基类。如果你必须上传,那么继承就是必须的,如果不需要上传,那么就该再看看是不是应该用继承了。
× Java 的关键词final 的含义会根据上下文略有不同,但是总的来说,它的意思都是“这样东西不允许改动”。
1、Final 的数据
编译时就可以计算的primitive数据:private final int i4 = rand.nextInt(20);
当final 不是指primitive,而是用于对象的reference 的时候, final 的意思则是这个reference 是常量。初
始化的时候,一旦将reference 连到了某个对象,那么它就再也不能指别的对象了。但是这个对象本身是可以修改的。如:private final Value v2 = new Value(22);
通常约定,被初始化为常量值的final static 的primitive 的名字全都用大写,词与词之间用下划线分开。
Java 能让你创建“空白的final 数据(blank finals)”,也就是说把数据成员声明成final 的,但却没给初始化的值,你必须先进行初始化,再使用空白的final 数据成员,而且编译器会强制你这么做。空白的final 数据也提供了一种更为灵活的运用final 关键词方法,如:
public class Dog{
private final String pinZhong;
public Dog(String pinZhong){
this,pinZhong = pinZhong;
}
}
Java 允许你在参数表中声明参数是final 的,这样,你不能在方法里让参数reference 指向另一个对象了,但是这种功能一般没什么用处:
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
}
2、final方法
使用final 方法的目的有二:
第一, 为方法上“锁”,禁止派生类进行修改。这是出于设计考虑。
第二个原因就是效率。如果方法是final 的,那么编译器就会把调用转换成“内联的(inline)”。当编译器看到
要调用final 方法的时候,它就会(根据判断)舍弃普通的,“插入方法调用代码的”编译机制(将参数压入栈,然后跳去执行要调用的方法的代码,再跳回来清空栈,再处理返回值),相反它会用方法本身的拷贝来代替方法的调用。当然如果方法很大,那么程序就会膨胀得很快,于是内联也不会带来什么性能的改善,因为这种改善相比程序处理所耗用的时间是微不足道的。Java 的设计者们暗示过,Java 的编译器有这个功能,可以智能地判断是不是应该将final 方法做成内联的。不过,最好还是把效率问题留给编译器和JVM去处理,而只把final 用于要明确地禁止覆写的场合。
private 方法都隐含有final 的意思。由于你不能访问private 的方法,因此你也不能覆写它。你可以给private 方法加一个final 修饰符,但是这样做什么意义也没有。
3、 final类
把整个类都定义成final 的就等于在宣布,你不会去继承这个类,你也不允许别人去继承这个类。
由于final 类禁止了继承,覆写方法已经不可能了,因此所有的方法都隐含地变成final 了。你可以为final 类的方法加一个final 修饰符,但是这一样没什么意义。
× 类的装载通常都发生在第一次创建那个类的对象的时候,但是访问static 数据或static 方法的时候也会装载。
× 继承情况下的初始化:
当装载器装载一个类时,如果发现这个类有一个基类,就会装载这个基类。不论是否创建这个基类,这个过程一定会发生。
如果基类还有基类,那么这第二个基类也会被装载,以此类推。
下一步,它会执行“根基类(root base class)”的static 初始化,然后是下一个派生类的static 初始化,以此类推。这个顺序非常重要,因为派生类的“静态初始化(即前面讲的static 初始化)”有可能要依赖基类成员的正确初始化。
现在所有必要的类都已经装载结束,可以创建对象了。
首先,对象里的所有的primitive 都会被设成它们的缺省值,而reference 也会被设成null——这个过程是一瞬间完成的,对象的内存会被统一地设置成“两进制的零(binary zero)”。
然后调用基类的构造函数。调用是自动发生的,但是你可以使用super 来指定调用哪个构造函数。基类的构造过程以及构造顺序,同派生类的相同。
基类构造函数运行完毕之后,会按照各个变量的字面顺序进行初始化。最后会执行构造函数的其余部分。
× 读懂下面这段代码:
class Insect{
private int i = 9;
protected int j;
Insect(){
System.out.println("i="+i+" j="+j);
j=39;
}
static int print(String s){
System.out.println(s);
return 47;
}
private static int x1 = print("static Insect.x1 initialized");
}
public class Beetle extends Insect{
private int k = print("Beetle.k initialized");
public Beetle(){
System.out.println("k="+k);
System.out.println("j="+j);
}
private static int x2 = print("static Beetle.x2 initialized");
public static void main(String[] args){
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
输出结果:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
解释如下:
1、由于这个编译单元的public类中包含有main()方法,可以使用java命令来执行这个主类。即使main()是个空方法,装载器也会装载这个主类,这时,发现这个主类还有一个基类,于是装载器又装载了了基类。这个过程象一个压栈的过程一样,然后,从基类开始,初始化类系中各个类的static成员变量、执行static块,但绝对不会执行类中的static方法(所有方法都是等着来人工调用的)。在这个过程中,也绝对不会涉及到类中的非static成员和方法,因为这些成员和方法根本无法加入static语句块。
2、加载完类之后,JVM开始从主类的main()方法一条条执行语句。在这些语句中,可能执行一些普通语句,也可能会创建某个类的对象。如果发现这个对象所属的类还有基类,那么会先调用基类的构造方法创建一个基类的对象,最后才会调用当前要创建的对象的构造方法。