继承主要有两个作用:
1.为导出类提供方法,这个特性在代码复用有比较多的应用
2.表现导出类与基类之间的关系。这个特性在多态里非常有用,后面会讲到。
我们先来讲为导出类提供方法这个特性——继承是代码复用中重要的一种方式,很难把它与复用割裂开看,而且放在一起比较似乎更容易理解继承在为导出类提供方法这方面的用途以及与其他两种方式的区别。那么这一篇我们讲组合&继承&代理——代码复用的思想实现。
复用代码主要有组合、继承、代理几种方式。关于这几种方式的使用,简单来说就是:
1.组合:在新类中嵌入某个对象,通过引用调用它的方法获得其功能,但是新类的用户看到的只是新类所定义的接口;
2.继承:从基类继承获得基类的全部非private属性和方法,即拥有与基类相同的特性。
3.代理:组合和继承的“中间”方法,嵌入其基类,然后在导出类的方法中通过基类的引用调用其方法获得功能,对外提供的还是导出类的方法
下面结合例子具体来讲讲组合、继承、和代理。
(一、组合
在编写代码过程中,将对象引用置于新类中的做法就是组合。新类只是需要它的功能,但是两者没有什么关联——组合体现的是一种整体与部分,一种拥有的关系。类似于现代化组装,每个对象都是一个零件,拥有各自的功能,组合起来的新类才是我们需要的产品,但是我们不需要去了解每一个零件。就比如造一台手机,用到CPU、GPU、ROM、等等。
如:
public class mobilePhone{
private MobilePhone mobilePhone{
CPU cpu = new CPU();
GPU gpu = new GPU();
ROM rom = new ROM();
}
}
组合的优点:
1.当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象是不可见的。
2.当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
3. 当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
缺点:
1.容易产生过多的对象。
(二、继承
以现有的类作为基类建立新的类,默认拥有父类全部的非private的属性和方法,不能选择性的继承,同时新类可以增加新的属性和方法或者覆盖基类的方法。它描述的是一种is-a描述的关系,是类与类,接口与接口之间最常见的关系。例如学生是个人,那么学生就可以继承人这个类。
这么说似乎同样有些抽象,那么下面我们来看例子:
public class BoyFriend{
private String name ;
private String sex ;
private int age ;
private GirlFriend girlfriend;
}
public class GirlFriend{
private String name ;
private String sex ;
private int age ;
private BoyFriend boyFriend;
}
我们定义了两个类,boyfriend和girlFriend,看两者的属性,我们不难发现,除了各自的boyFriend和girlFriend不同外,其他字段完全相同,如果这个时候我们来个其他身份,我们是不是又要重写一遍name,age,sex等重复的属性值呢,所以这里就可以引入继承特性实现复用。
在使用继承特性之前,我们需要想清楚一点,他们继承什么,也就是他们的共性是什么,比如这里的男女朋友,大人、小孩,都是人,都有各自的名字、年龄、性别等等,并且都有相同的行为吃饭、走路、说话等等,所以我们认为他们都拥有人的属性和行为,同时也应该继承这些属性和行为。
通过extends关键字表达类的继承关系。
public class Human{
/*
* 对属性的封装
* 姓名、性别、年龄都是这个人的私有属性
*/
private String name ;
private String sex ;
private int age ;
/*
* setter()、getter()是该对象对外开放的接口,此处省略setter、getter方法
*/
}
public class BoyFriend extends Human{
private GirlFriend girlFriend;
}
public class GirlFriend extends Human{
private BoyFriend boyFriend;
}
使用继承后,代码量减少的同时,我们还能够非常清晰的看到他们的关系。
继承所描述的是“is-a”的关系,如果有两个对象A和B,可以描述为“A是B”,则可以表示A继承B,其中B被A称为父类或者超类,A是B的子类或者派生类。
优点:
使子类可以重写父类的方法来方便地实现对父类的扩展。
缺点:
1.因为子类继承了父类全部非private属性和方法—父类的内部细节对子类是可见的。
2.子类从父类继承的方法在编译时就确定下来了——继承后,在调用导出类构造器的时候,会插入对基类的构造器的调用,所以无法在运行期间改变从父类继承的方法的行为。
3.如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
关于调用子类构造器插入父类构造器,我们来看下面这段代码:
Class Human{
Human(){
System.out.println("Person Constrctor-----Human" );
}
}
public class BoyFriend extends Human{![在这里插入图片描述](https://img-blog.csdnimg.cn/20181113125049952.png)
private GirlFriend girlFriend ;
BoyFriend (){
System.out.println("BoyFriend Constructor...");
}
public static void main(String[] args) {
BoyFriend boyFriend = new BoyFriend ();
}
}
Output:
上面这个例子我们可以看到,构建过程是从父类开始向子类一级一级的完成构建。在我们并没有显式的引用父类的构造器时,编译器仍会默认给子类调用父类的构造器。如果我们显式的定义了构造器,编译器就不会自动添加构造器了
注意:所有的构造器默认为static
但是调用父类的无参构造器是有前提的:父类有无参构造器。如果父类没有默认无参构造器,我们就必须显示的使用super()来调用父类构造器,否则编译器会报错
所以《Thinking in Java》中关于是否使用继承有这么一句话:
问一问自己是否需要从子类向父类进行向上转型,如果必须向上转型,则继承是必要的,但是如果不需要,则应当考虑自己是否需要继承。(现在可能有点看不懂,看到多态一篇就明白了)
![](/qrcode.jpg)
(三、代理
代理是组合与继承结合的中间方法,在新类中new一个存在的父类对象(组合的做法),此时向新类暴露了对象的全部接口(继承的结果)。但是有所不同的是,此时代理可以选择性的开放拥有的接口。借用thinking in Java上面的例子:
final class PlaneControl {//final表示不可继承
protected void speed() {}
protected void fire() {}
protected void left() {}
protected void right() {}
}
public class DelegationTest{
private PlaneControl planeControl; //private外部不可访问
/*
* 飞行员权限代理类,普通飞行员不可以开火
*/
PlaneDelegation(){
planeControl=new PlaneControl();
}
public void speed(){
planeControl.speed();
}
public void left(){
planeControl.left();
}
public void right(){
planeControl.right();
}
}
可以看到基类的接口通过组合的方式全部获得了,然后对外提供了想提供的部分,外界无法获得全部的基类方法(这里是简化版的静态代理方法,具体静态代理与动态代理的对比后面会详细聊聊)。
所以总得来说:
组合:如果只是需要使用父类的功能,可以选用组合。
继承:如果想要向上转型为父类,使用多态,就使用继承。
代理:如果想使用父类方法,但是又不想暴露全部接口,使用代理。