首页 > 技术文章 > 继承

xidongyu 2017-06-03 15:13 原文

本文主要讲继承原理上的一些东西,关于继承的细节为什么要这样设计,如果没有基础的朋友请先补充一下知识空白。

继承的作用

首先我们先明白为啥要用继承,有助于对继承中知识点的理解。有了继承之后,当我们做功能扩展时,可以少写代码,这是最直接的好处。比如已有一个类,要为其添加新的功能又破坏其结构,我们可以重写一个新的类让其继承原来的类,然后在新的类上添加新功能,这样就可以减少重复代码,下面是一个简单的例子:

class Base {
	public void walk(){}
}

class Foo extends Base {
	public void run(){}
}

上面的代码中Foo继承Base后,在Foo添加新的功能run,那么Foo就同时拥有了walk和run的功能

为什么要调用父类构造器

在子类构造器中必须隐式或显示的调用父类的构造器,其作用是对子类的部分成员属性的初始化。这句话有点难理解,因为我们固有的概念是自己的成员属性一般都是在自己的构造器内初始化,用别的类的构造器初始化自己成员属性显得有些诡异。我们先来考虑这样一个问题,当子类继承父类后就会继承父类的属性,那么子类是否有必要在自己构造器内对继承来的属性重新做初始化操作,如下面的例子

class Base {
	int meter;
	
	public Base() {
		meter = 2;
	}
}

class Foo extends Base {

	public Foo() {
		meter =2;
	}
}

上面的代码中父类对meter进行初始化,子类继承了meter,所以本着自己的属性在自己构造器中初始化的思想,我们在Foo对meter进行了初始化话操作。这个还比较简单,万一父类Base中的属性很多,比如好几百个,那么我们在子类做这些属性初始化操作岂不是累死。这时处于简化代码的考虑,调用父类的构造器,让其帮忙初始化自己的继承来的成员属性,这样就好多了。

当然上面的解释是笔者自己的想法,但是我认为是有道理的。还有一种的解释是,子类不知道如何初始化继承来的属性,甚至无法初始化继承来的属性(私有属性),那么只能调用父类的构造器来初始化。

对象到底占多少内存

当我们实例化一个子类对象后,这个对象到底有多大呢?这是在java中是无法回答的,因为java没有测量一个对象大小的工具,不想C/C++中sizeof,但是我们可以从宏观上进行分析

隐藏

我们经常说私有属性无法被继承其实不够严格,其实私有属性是能被继承的,只是在子类中被隐藏起来了

class Base {
	private int meter = 10;
	public void  print() {
		System.out.println(meter);
	}
}

class Foo extends Base {

}

public class Test{
	public static void main(String[] args) {
		new Foo().print();
	}
}

在上面的代码中我们将meter设置为私有属性,print方法会被Foo继承下来,并且在main中创建Foo对象并调用了print方法。如果meter没有被Foo继承下来,那么Foo就不会有meter属性,print就不会执行成功,但是上面的代码输出结果为10

我们再来看一段代码

class Base {
	int meter = 10;
}

class Foo extends Base {
	int meter = 20;
}

public class Test{
	public static void main(String[] args) {
		Foo foo = new Foo();
		Base base = foo;
		System.out.println(foo.meter);
		System.out.println(base.meter);
	}
}

输出结果

20
10

上面的代码中子类声明了一个和父类同名的属性,但是在主函数我们做了写处理就可以输出不同的结果,其实这也是隐藏。父类的meter仍被子类继承了下来,由于有一个同名的属性,导致这个meter在子类中被隐藏起来。我们可以通过向上转型来访问到这个隐藏的属性。

对象和子对象

从上面可以看出在任何情况下,子类都将会继承父类的全部属性。因此在为子类对象分配的内存中有一部分存储的是继承于父类的属性,我们把这一部分属性称为子对象(这个子对象根本不存在,是假想出来的)。当我们调用父类的构造函数时,其实是对子对象进行初始化。其余的属性应该子类构造来完成初始化。因此,对子类对象的初始化应该分两部分来完成,一部分由父构造来初始化(继承的属性),一部分由自己的构造器初始化(自己定义的属性)

小结

一个对象占据的内存大小应该为父类中所有属性的总和加上自己定义的属性综合。该问题其实对上一个问题的补充说明。

静态绑定与动态绑定

静态绑定和动态绑定属于多态的内容,但是又与继承息息相关,这里提一下。
属性和被final、static和private修饰的方法采用静态绑定的方式,其他方法采用动态绑定。关于绑定的问题请参考http://www.cnblogs.com/xidongyu/p/6930316.html

其他注意事项

关于复写
class Base {

	A p(){return new A();}
}

class Foo extends Base {

	B p(){return new B();}
}

上面的代码中子类复写了父类的p方法,但是返回值却不同,这样一般会编译报错。但是有一种特殊情况就是如果B是A的子类,那么会编译成功

推荐阅读