复用类
继承语法
- 由于涉及基类和导出类两个类,而不是只有一个类,所以要试着想像导出类所产生的结果对象,会有点困惑。从外部来看,它就像是一个与基类具有相同接口的新类,或许还有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象和你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。
- 那么该如何对基类的子对象进行初始化呢?我们可以在导出类的构造器中调用基类的构造器来执行初始化。Java会自动在导出类的构造器中插入对基类构造器的调用。
public class Test{
public static void main(String s[]) {
new GrandSon();
}
}
class GrandFather {
public GrandFather() {
System.out.println("I'm GrandFather");
}
}
class Father extends GrandFather {
public Father() {
System.out.println("I'm Father");
}
}
class GrandSon extends Father {
public GrandSon() {
System.out.println("I'm GrandSon");
}
}
----------------- 运行结果:
I'm GrandFather
I'm Father
I'm GrandSon
- 除了从结果可以说明自动插入了
super();
代码外,我们可以修改一下GrandFather 类,移除默认构造器,增加GrandFather (String)构造器,你就会发现编辑器会报错。
Implicit super constructor GrandFather() is undefined. Must explicitly invoke another constructor.
//没有定义隐式调用的基类构造函数grandfather()。必须显式调用另一个构造函数。
- 可见导出类的构造器是必须调用基类的构造器的(隐式或显式)。
- 还记得前面讨论过的this吗?
String name;
public void setName(String name) {
this.name = name;
}
- 那关于继承,如果子类和父类拥有同名变量或者同名方法,我该如何随心所欲的调用他们呢?相信看了下面的例子,就会明白应该怎么调用了吧。关键就是要学会使用super关键字。此外如果导出类中没有相应的方法名,如果存在基类,它则会自动加上super去查找基类中的方法(前提必须有访问权限)。这个行为和自动加上this很类似。
public class Test{
public static void main(String s[]) {
GrandSon gs = new GrandSon();
System.out.println(gs.name);
System.out.println(gs.getFatherName());
System.out.println(gs.getGrandFatherName());
}
}
class GrandFather {
protected String name = "GF";
}
class Father extends GrandFather {
protected String name = "F";
protected String getGrandFatherName() {
return super.name;
}
}
class GrandSon extends Father {
public String name = "GS";
public String getFatherName() {
return super.name;
}
public String getGrandFatherName() {
return super.getGrandFatherName();
}
}
代理
- 工作中代理最常用的地方就是工具包了。举个例子,比如我们有一个专门处理字符串的工具类:StringUtil类。现在我们有一个需求,在不修改StringUtil类的前提下新建一个StringExpandUtil类,该类除了拥有StringUtil类所有静态方法和成员方法外,还需要自定义一些新的方法。你可能想到利用继承就可以解决这个问题了。但是继承有一个不好的地方。因为此处的需求是照搬StringUtil类中原有的方法。而继承可以在子类中重写原有的方法,这显然不符合原来的意图。此时通过代理就可以解决这个问题了(虽然代理也可以继续改进原有方法,但是它至少不会破坏原有的功能)。其实代理很简单,如果想要调用StringUtil里原有的方法,你直接通过StringUtil实例去调用就行了(静态方法通过类名去调用)。
public class Test{
public static void main(String s[]) {
StringExpandUtil.sysout("hello world");
StringExpandUtil.myStaticMethod();
StringExpandUtil seu = new StringExpandUtil();
System.out.println(seu.returnS());
seu.myMethod();
}
}
class StringUtil {
public String s = "StringUtil";
public static void sysout(String s) {
System.out.println(s);
}
public String returnS() {
return s;
}
}
class StringExpandUtil {
private StringUtil stringUtil = new StringUtil();
public static void sysout(String s) {
StringUtil.sysout(s);
}
public String returnS() {
return stringUtil.returnS();
}
public void myMethod() {
System.out.println("my method");
}
public static void myStaticMethod() {
System.out.println("my static method");
}
}
在组合和继承之间选择
- 组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。
- 关于何时使用组合,何时使用继承。举个例子,有一个车类和一个交通工具类。我们只会说车是一种交通工具(is-a关系)。而不会说车包含交通工具(has-a关系)。前者就是继承的关系,后者则是组合的关系。
向上转型
- “为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
- 由于基类中有的方法导出类同样也有,所以继承可以确保所有能够向基类发送的信息同样也可以发送给导出类。
public class Test{
public static void main(String s[]) {
doSayHi(new GrandSon());
doSayHi(new Father());
}
public static void doSayHi(GrandFather gf) {
gf.sayHi();
}
}
class GrandFather {
protected void sayHi() {
System.out.println("hi");
}
}
...省略部分代码
- 虽然doSayHi定义的形参类型是GrandFather,我们依旧可以传入GrandSon和Father实例。之前学过Java传递给方法的都是值的副本,那么在这里其实就是:
GrandSon gf = new GrandSon();
GrandFather _gf = gf;
_gf.sayHi();
- 向上转型是从一个较专用类型向较通用类型的转换,所以总是很安全的。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法。其实何时要使用继承,是否需要向上转型也是一个很好的评估手段。如果一个导出类根本用不到向上转型,那么就得重新考虑一下结构设计是否合理。
- 关于上转型对象的性质可以用以下两点来归纳:
- 上转型对象不能操作导出类新增加的成员变量,不能使用导出类新增的方法。这个我想大家都明白,既然已经声明为GrandFather类型,它自然不能使用GrandFather中没有定义的方法和变量。
- 上转型对象可以操作导出类继承或隐藏的成员变量,也可以使用导出类继承的或重写的方法。首先,如果导出类压根没有隐藏过基类的成员或重写基类的方法,那肯定是使用基类原有的成员或方法。如果隐藏变量了或者重写了方法,上转型对象对这两种有不一样的行为。它会始终调用父类的变量,使用导出类重写的方法。我觉得可以这样想:首先创建的导出类对象中有基类的子对象。然后向上转型,就是把这个子对象拿出来,把方法用导出类对象覆盖一遍,就得到上转型对象了。
- 除了向上转型,还可以向下转型。例如:
GrandSon gf = new GrandSon();
GrandFather _gf = gf;
GrandSon __gf = (GrandSon)_gf;
System.out.println(gf == _gf);
System.out.println(gf == __gf);
- 通过后面两个true,也就知道了其实对象还是那个对象,只是引用的类型不一样而已。可见引用的类型能够决定对象会被如何使用。举一个例子:比如说有动物类,和人类。
public class Test{
public static void main(String s[]) {
Animal al = new People();
System.out.println(al.type);
al.run();
}
}
class Animal {
String type = "动物";
void run() {
System.out.println("animal run");
}
}
class People extends Animal {
String type = "人类";
void run() {
System.out.println("people run");
}
}
- 你指着一个人说他是动物(假设你没有见过人类)。你让他run,他本质依旧是个人,所以会用人的方式去run,你问他的type是什么?因为你已经认定他是动物了,所以他得告诉你身为动物应有的type(否则按照你的认知,你根本不知道人类是什么)。讲的通俗一点,调用方法必须再通过对象,对象则会用自己最新的方式去执行方法。访问变量是以基类的名义去访问,那他就必然告诉你基类拥有的属性。
- 反正说来说去,只不过是为了记住这个性质,规则都是人定的嘛,强行解释也不是不可以。
final关键字
- final可能用到的三种情况:数据,方法和类。final通常指一个东西无法被改变,达到设计或效率的目的。final我用的少,所以要记得清楚点。
final数据
- 许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:
- 一个永不改变的编译时常量。
- 一个在运行时被初始化的值,而你不希望它被改变。
- 对于编译时常量,编译器可以将该常量值代入任何可能用它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。
- 对于基本类型,final使数值恒定不变。对于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改成指向另一个对象。但是对象本身是可以被改变的。同样的,数组引用不可被改变,但是每个元素(基本类型值或者对象引用)却是可以改变的。
- 必须在域的定义处(如果是static变量则只能在此处赋值)或者每个构造器中用表达式对final进行赋值。
final方法
- 使用final方法的原因有两个。一个是出于效率,一个是不希望被任何继承类修改它的定义。在Java的早期实现中,将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断(不知道是如何谨慎的?),跳过正常的方法调用机制(一般方法调用需要将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),然后以方法体中的实际代码的副本来替代方法调用。看下面的例子就知道了:
int x = method();
int method()
//内嵌调用就是将上述代码直接转换成:
int x = 1 + 2;
- 不过这种方式,如果遇到一个大方法,程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为所带来的性能提高会因为花费于方法内的时间量而被缩减(也许编译器得谨慎考虑如何将冗长的代码嵌进去?)。
- 故在最近的Java版本中,虚拟机可以探测这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此也不再需要final方法来进行优化了。所以目前要使用final方法都是出于后一个原因(效率的问题交给编译器和JVM呗)。
- 类中所有的private方法都隐式地指定为final的。因为无法取用private方法,所以也就无法覆盖它。也许你会好奇为什么下面的方法不会报错:
class Animal {
String type = "动物";
private void run() {
System.out.println("animal run");
}
}
class People extends Animal {
String type = "人类";
void run() {
System.out.println("people run");
}
}
- final方法无法被覆盖我们都知道,那private方法既然有隐式的final修饰,那为什么此处可以被覆盖呢?这里真的是覆盖吗?如果你加上@Override就会发现其实它并不是覆盖。private的方法意义仅限于基类内部,对于导出类来说这是根本看不见的。导出类不过是管自己定义了一个普通方法而已。基类也是比较宽容的,它也认为这是导出类独有的方法,自己的小九九就不拿出来炫耀了(private)。更专业一点的说,覆盖只有在某方法是基类的接口的一部分才会出现。再举个例子,如果基类有个友好方法,而导出类与基类不在一个包下的话,那导出类同样无法覆盖这个方法。只能覆盖有访问权限的方法。
final类
- final类即不希望该类被继承。所有final类中的方法都会隐式指定为final。但是成员的话还是该怎样就怎样(不要以为成员也会自动定义为final)。
final小结
- 个人觉得final方法和final类没什么用。除非你有很好的设计,不然妄自给类或者方法加上final,极大可能影响后续方法或者类的扩展。
继承与初始化
- 前面文章已经稍稍讲了一些类的初始化顺序(static变量在首次访问类被创建并初始化,非static成员在创建对象的时候初始化)。那么首次创建一个导出类对象的初始化过程会是如何的呢?看下面的例子:
public class Beetle extends Insect {
String s1 = FZ.print("Beetle 字段处初始化");
static String s2 = FZ.print("Beetle static 字段处初始化");
public Beetle() {
System.out.println("Beetle 构造器初始化");
}
public static void main(String args[]) {
System.out.println("Beetle main方法运行");
new Beetle();
}
}
class Insect {
String s1 = FZ.print("Insect 字段处初始化");
static String s2 = FZ.print("Insect static 字段处初始化");
public Insect() {
System.out.println("Insect 构造器初始化");
}
}
class FZ {
static String print(String message) {
System.out.println(message);
return "没有用";
}
}
--------------------运行结果如下:
Insect static 字段处初始化
Beetle static 字段处初始化
Beetle main方法运行
Insect 字段处初始化
Insect 构造器初始化
Beetle 字段处初始化
Beetle 构造器初始化
- 在Beetle上运行Java时,所发生的第一件事就是试图访问Beetle.main(),于是加载器开始启动并找出Beetle类的编译代码(在Beetle.class中)。在对它进行加载的过程中,编译器注意到它有一个基类(由extends关键字得知),于是它继续加载Insect类的编译代码。如果它还有基类,那么第二个基类就会被加载,以此类推。接下来进行根基类的static初始化,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。
- 至此为止,必要的类都已经加载完毕,可以进行对象的创建了。如果该类是导出类,先是基类的构造器会被调用(在构造器的首行默认会调用super())。如果基类也有基类,则再调用基类的基类的构造器,以此类推。接下来进行根基类的字段处初始化,再进行构造器中的初始化,然后是下一个导出类的重复步骤,以此类推。