--- inheritance object-oriented classes: Java foundation

The last chapter, we talked about how the concept of mapping the reality of the concept of the program, we talk about the combination between class and class, between the concept of reality there is a very important relationship, is classified . Classification roots, and then continue to refine down to form a hierarchical classification system. This example is very much:

1) In the natural world, there are biological animals and plants, animals have different subjects, carnivores, herbivores, omnivores and other predators are wolves, dogs, tigers, etc., which are divided into different varieties.

2) Open the electricity supplier site, in general classification list in a prominent position, such as household appliances, clothing, clothing has a women's, men, men have shirts, jeans and so on ...

The computer program is often used between classes inheritance to represent the relationship between object-relational classification. In the hierarchy, there is the parent class and subclass , such as dogs and animal Animal Dog, Animal is the parent class, Dog is a subclass. Also called parent base class , subclass, also called a derived class , the subclass is the parent class opposite, class B may be a subclass of the class A and class C is the parent class.

The reason is called inheritance, because the subclass inherits the properties and behavior of the parent, the parent class attributes and behavior of some subclasses have . But subclass can increase subclass-specific attributes and behaviors , some parent class, some behaviors, implementation subclass may not be exactly the same as the parent class.

Using inheritance on the one hand can reuse the code, public properties and behavior can be placed in the parent class, and subclass-specific subclass only need to focus on it; on the other hand, objects of different subclasses can be more convenient unified treatment.

This detailed study asked inheritance. After we first introduce the basic concepts of inheritance, succession and then detailed some of the details, understanding the inheritance of usage, we are talking about inheritance considerations explain why inheritance is a double-edged sword, and how to properly use inheritance.

# A. The basic concept
## 1. Root class Object

In Java, all classes have a parent, even if the parent does not declare, there is an implicit parent, the parent class called Object. Object attributes are not defined, but the definition of a number of methods, as shown below:
Write pictures described here

In this section we will introduce toString () method, otherwise we will be gradually introduced in the following sections. Objective toString () method returns an object that is a text description of this method can be directly used for all classes.

For example, for the Point class we introduced earlier, so you can use the toString method:

Point p = new Point(2,3);
System.out.println(p.toString());

Output like this:

Point@76f9aa66

What does it mean? Before the @ is the class name after the @ What is it? We look at the toString code:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

getClass (). getName () returns the current object's class name , hashCode () returns an object hash value, the hash we will introduce in later chapters, here it can be understood as an integer that by default, usually the memory address value of the object, Integer.toHexString (hashCode ()) return shiliu hexadecimal representation of the hash value.

Why you wrote it? Write the class name is understandable, represents the type of object, and write hash value is a last resort, because the Object class does not know the specific object properties, do not know how to use a text description, but need to distinguish between different objects, can only write a hash value.

But subclass is known subclass own property can override the parent method to reflect their different implementations. The so-called rewrite, is the same as the parent class definition and method, and reimplemented.

## 2. The method of overwriting

On one, we introduce some of the graphics category, including the Point class, this time we rewrite the toString () method. Code below (Point.class):

public class Point {

    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }
    
    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }

}

toString method has a front @Override , which represents toString This method is a method of rewriting the parent class (of course, may be a parent's parent class), the method returns the value after overwriting Point x and y coordinates. After rewriting, the call implementation subclass. For example, the following code output becomes: (2,3)

Point p = new Point(2,3);
System.out.println(p.toString());

## 3 graphics class inheritance system

Next, we further explained with examples of some of the graphics processing, first look at the figures:

Write pictures described here

These are some of the basic pattern, a wired pattern, square, triangular, circular, etc., have a different color pattern. Next, we define the following class to demonstrate some of the concepts of inheritance:

  • Parent class Shape, a graphical representation.
  • Class Circle, represents the circle.
  • Class Line, represent a straight line.
  • Class ArrowLine, with an arrow represents a linear,

1. Graphics
All graphics (the Shape) has a color represented by the attribute, there is a representation of the drawing, as follows (Shape.class):

public class Shape {
	
	private static final String DEFAULT_COLOR = "black";
	
	private String color;
	
	public Shape() {
		this(DEFAULT_COLOR);
	}

	public Shape(String color) {
		this.color = color;
	}
	
	public String getColor() {
		return color;
	}

	public void setColor(String color) {
		this.color = color;
	}
	
	public void draw(){
		System.out.println("draw shape");
	}
}

The above code basically no interpretable, instance variables color indicates the color, draw method represents a draw, we will not write the actual rendering code, mainly to demonstrate inheritance.

2. circularly
circularly (Circle) inherited from the Shape, but includes additional attributes, center point and radius, as well as additional methods Area, the area used to calculate, in addition, a draw rewritten as the following code:

public class Circle extends Shape {
    //中心点
    private Point center;

    //半径
    private double r;

    public Circle(Point center, double r) {
        this.center = center;
        this.r = r;
    }

    @Override
    public void draw() {
        System.out.println("draw circle at "
                + center.toString() + " with r " + r
                + ", using color : " + getColor());
    }

    public double area() {
        return Math.PI * r * r;
    }

}

Description:

  1. Java uses the extends keyword marked inheritance, a class can only have at most one parent;
    2) sub-class can not directly access private properties and methods of the parent class, for example, in Circle, you can not access the private instance variable color shape directly;
    3) in addition to private, the subclass inherits methods and other properties of the parent class, for example, in the draw method Circle, can call the getColor () method directly.

Let's verify. Code below (chapter_4Activity.class):

Point center = new Point(2,3);
//创建圆,赋值给circle
Circle circle = new Circle(center,2);
//调用draw方法,会执行Circle的draw方法
circle.draw();
//输出圆面积
System.out.println(circle.area());

Output of the program is:

draw circle at (2,3) with r 2.0, using color : black
12.566370614359172

Here is a rather strange, color is assigned when? In the new process, the parent class constructor will be performed, and priority subclass in the first implementation. In this example, the default constructor of the superclass Shape constructor is executed before the subclass of Circle. Details about the new process, we will be further described in the following sections.

3.直线
线(Line)继承自Shape,但有两个点,有一个获取长度的方法,另外,重写了draw方法,代码如下(Line.class):

public class Line extends Shape {
    private Point start;
    private Point end;

    public Line(Point start, Point end, String color) {
        super(color);
        this.start = start;
        this.end = end;
    }

    public double length() {
        return start.distance(end);
    }

    public Point getStart() {
        return start;
    }

    public Point getEnd() {
        return end;
    }

    @Override
    public void draw() {
        System.out.println("draw line from "
                + start.toString() + " to " + end.toString()
                + ",using color " + super.getColor());
    }
}

这里我们要说明的是super这个关键字,super用于指代父类,可用于调用父类构造方法,访问父类方法和变量。
1)在Line构造方法中,super(color)表示调用父类的带color参数的构造方法,调用父类构造方法时,super(…)必须放在第一行。
2)在draw方法中,super.getColor()表示调用父类的getColor方法,当然不写super.也是可以的,因为这个方法子类没有同名的,没有歧义,当有歧义的时候,通过super.可以明确表示调用父类的。
3)super同样可以引用父类非私有的变量。

可以看出,super的使用与this有点像,但super和this是不同的,this引用一个对象,是实实在在存在的,可以作为函数参数,可以作为返回值,但super只是一个关键字,不能作为参数和返回值,它只是用于告诉编译器访问父类的相关变量和方法。

4.带箭头直线
带箭头直线 (ArrowLine)继承自Line,但多了两个属性,分别表示两端是否有箭头,也重写了draw方法,代码如下(ArrowLine.class):


public class ArrowLine extends Line {
	
	private boolean startArrow;
	private boolean endArrow;
	
	public ArrowLine(Point start, Point end, String color, 
			boolean startArrow, boolean endArrow) {
		super(start, end, color);
		this.startArrow = startArrow;
		this.endArrow = endArrow;
	}

	@Override
	public void draw() {
		super.draw();
		if(startArrow){
			System.out.println("draw start arrow");
		}
		if(endArrow){
			System.out.println("draw end arrow");
		}
	}
}

ArrowLine继承自Line,而Line继承自Shape,ArrowLine的对象也有Shape的属性和方法。

注意draw方法的第一行,super.draw()表示调用父类的draw()方法,这时候不带super.是不行的,因为当前的方法也叫draw()

需要说明的是,这里ArrowLine继承了Line,也可以直接在类Line里加上属性,而不需要单独设计一个类ArrowLine,这里主要是演示继承的层次性。

5.图形管理器
使用继承的一个好处是可以统一处理不同子类型的对象。比如,我们来看一个图形管理者类,它负责管理画板上的所有图形对象并负责绘制,在绘制代码中,只需要将每个对象当做Shape并调用draw方法就可以了,系统会自动执行子类的draw方法。代码如下(ShapeManager.class):

public class ShapeManager {
    
    private static final int     MAX_NUM  = 100;
    private              Shape[] shapes   = new Shape[MAX_NUM];
    private              int     shapeNum = 0;

    public void addShape(Shape shape) {
        if (shapeNum < MAX_NUM) {
            shapes[shapeNum++] = shape;
        }
    }

    public void draw() {
        for (int i = 0; i < shapeNum; i++) {
            shapes[i].draw();
        }
    }
}

ShapeManager使用一个数组保存所有的shape,在draw方法中调用每个shape的draw方法。ShapeManager并不知道每个shape具体的类型,也不关心,但可以调用到子类的draw方法。

我们来看下使用ShapeManager的一个例子。代码如下(chapter_4Activity.class):

ShapeManager manager = new ShapeManager();
manager.addShape(new Circle(new Point(4,4),3));
manager.addShape(new Line(new Point(2,3),new Point(3,4),"green"));
manager.addShape(new ArrowLine(new Point(1,2),new Point(5,5),"black",false,true));
manager.draw();

新建了三个shape,分别是一个圆、直线和带箭头的线,然后加到了shape manager中,然后调用manager的draw方法。

需要说明的是,在addShape方法中,参数Shape shape,声明的类型是Shape,而实际的类型则分别是Circle,Line和ArrowLine。子类对象赋值给父类引用变量,这叫向上转型,转型就是转换类型,向上转型就是转换为父类类型

变量shape可以引用任何Shape子类类型的对象,这叫多态,即一种类型的变量,可引用多种实际类型对象。这样,对于变量shape,它就有两个类型,类型Shape,我们称之为shape的静态类型,类型Circle/Line/ArrowLine,我们称之为shape的动态类型。在ShapeManager的draw方法中,shapes[i].draw()调用的是其对应动态类型的draw方法,这称之为方法的动态绑定

为什么要有多态和动态绑定呢?创建对象的代码 (ShapeManager以外的代码)和操作对象的代码(ShapeManager本身的代码),经常不在一起,操作对象的代码往往只知道对象是某种父类型,也往往只需要知道它是某种父类型就可以了。

可以说,多态和动态绑定是计算机程序的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为

##4.小结

上面我们学习了继承和多态的基本概念。

1)每个类有且只有一个父类,没有声明父类的其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,可以重写父类的方法实现。

2)new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的话,调用父类的默认构造方法。

3)子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。

4)子类对象可以赋值给父类引用变量,这叫多态,实际执行调用的是子类实现,这叫动态绑定。

#二.继承的细节
##1.构造方法
前面我们说过,子类可以通过super调用父类的构造方法,如果子类没有通过super调用,则会自动调动父类的默认构造方法,那如果父类没有默认构造方法呢?如下所示:

public class Base {
    private String member;
    public Base(String member){
        this.member = member;
    }
}

这个类只有一个带参数的构造方法,没有默认构造方法。这个时候,它的任何子类都必须在构造方法中通过super调用Base的带参数构造方法,如下所示,否则,Java会提示编译错误。

public class Child extends Base {
    public Child(String member) {
        super(member);
    }
}

另外需要注意的是,如果在父类构造方法中调用了可被重写的方法,则可能会出现意想不到的结果,我们来看个例子,下面是基类代码(Base.class):

public class Base {
    public Base(){
        test();
    }
    
    public void test(){
    }
}

构造方法调用了test()。这是子类代码(Child.class):

public class Child extends Base {
    private int a = 123;
    
    public Child(){
    }
    
    public void test(){
       Log.e(TAG, "构造方法............" + a);
    }
}

子类有一个实例变量a,初始赋值为123,重写了test方法,输出a的值。看下使用的代码(chapter_4Activity.class):

Child c = new Child();
c.test();

输出结果是:

构造方法............0
构造方法............123

第一次输出为0,第二次为123。第一行为什么是0呢?第一次输出是在new过程中输出的,在new过程中,首先是初始化父类,父类构造方法调用test(),test被子类重写了,就会调用子类的test()方法,子类方法访问子类实例变量a,而这个时候子类的实例变量的赋值语句和构造方法还没有执行,所以输出的是其默认值0。

像这样,在父类构造方法中调用可被子类重写的方法,是一种不好的实践,容易引起混淆,应该只调用private的方法。

##2.重名与静态绑定

前面我们说到,子类可以重写父类非private的方法,当调用的时候,会动态绑定,执行子类的方法。那实例变量、静态方法、和静态变量呢?它们可以重名吗?如果重名,访问的是哪一个呢?

重名是可以的,重名后实际上有两个变量或方法。对于private变量和方法,它们只能在类内被访问,访问的也永远是当前类的,即在子类中,访问的是子类的,在父类中,访问的父类的,它们只是碰巧名字一样而已,没有任何关系。

但对于public变量和方法,则要看如何访问它,在类内访问的是当前类的,但子类可以通过super.明确指定访问父类的。在类外,则要看访问变量的静态类型,静态类型是父类,则访问父类的变量和方法,静态类型是子类,则访问的是子类的变量和方法。我们来看个例子:

这是基类代码(Base1.class):

public class Base1 {

    private static final String TAG = "Base1";
    public static        String s   = "static_base";

    public String m = "base";

    public static void staticTest() {
        Log.e(TAG, "重名与静态绑定....."+"base static: " + s);
    }

}

定义了一个public静态变量s、一个public实例变量m、一个静态方法staticTest。

这是子类代码(Child1.class):

public class Child1 extends Base1 {

    private static final String TAG = "Base1";

    public static String s = "child_base";
    public        String m = "child";

    public static void staticTest(){
        Log.e(TAG, "重名与静态绑定....."+"child static: " + s);
    }

}

子类定义了和父类重名的变量和方法。对于一个子类对象,它就有了两份变量和方法,在子类内部访问的时候,访问的是子类的,或者说,子类变量和方法隐藏了父类对应的变量和方法,下面看一下外部访问的代码(chapter_4Activity.class):

    Child1 c1 = new Child1();
    Base1 b1 = c1;
    Log.e(TAG, "重名与静态绑定....."+"外部调用: " + b1.s);
    Log.e(TAG, "重名与静态绑定....."+"外部调用: : " + b1.m);
    b1.staticTest();
    Log.e(TAG, "重名与静态绑定....."+"外部调用: " + c1.s);
    Log.e(TAG, "重名与静态绑定....."+"外部调用: : " + c1.m);
    c1.staticTest();

以上代码创建了一个子类对象,然后将对象分别赋值给了子类引用变量c1和父类引用变量b1,然后通过b1和c1分别引用变量和方法。这里需要说明的是,静态变量和静态方法一般通过类名直接访问,但也可以通过类的对象访问。程序输出为:

E/chapter_4Activity: 重名与静态绑定.....外部调用:       static_base
                     重名与静态绑定.....外部调用: :     base
E/Base1:             重名与静态绑定.....base static:   static_base
E/chapter_4Activity: 重名与静态绑定.....外部调用:       child_base
                     重名与静态绑定.....外部调用: :     child
E/Base1:             重名与静态绑定.....child static:  child_base

当通过b1 (静态类型Base) 访问时,访问的是Base的变量和方法,当通过c1 (静态类型Child)访问时,访问的是Child的变量和方法,这称之为静态绑定,即访问绑定到变量的静态类型。静态绑定在程序编译阶段即可决定,而动态绑定则要等到程序运行时。实例变量、静态变量、静态方法、private方法,都是静态绑定的

##2.重载和重写
重载是指方法名称相同但参数签名不同(参数个数或类型或顺序不同),重写是指子类重写父类相同参数签名的方法。对一个函数调用而言,可能有多个匹配的方法,有时候选择哪一个并不是那么明显,我们来看个例子:

这里基类代码:

public class Base {
    public int sum(int a, int b){
        System.out.println("base_int_int");
        return a+b;
    }
}

它定义了方法sum,下面是子类代码:

public class Child extends Base {
    public long sum(long a, long b){
        System.out.println("child_long_long");
        return a+b;
    }
}

以下是调用的代码:

public static void main(String[] args){
    Child c = new Child();
    int a = 2;
    int b = 3;
    c.sum(a, b);
}

这个调用的是哪个sum方法呢?每个sum方法都是兼容的,int类型可以自动转型为long,当只有一个方法的时候,那个方法就会被调用。但现在有多个方法可用,子类的sum方法参数类型虽然不完全匹配但是是兼容的,父类的sum方法参数类型是完全匹配的。程序输出为:
base_int_int

父类类型完全匹配的方法被调用了。如果父类代码改成下面这样呢?

public class Base {
    public long sum(int a, long b){
        System.out.println("base_int_long");
        return a+b;
    }
}

父类方法类型也不完全匹配了。程序输出为:
base_int_long

调用的还是父类的方法。父类和子类的两个方法的类型都不完全匹配,为什么调用父类的呢?因为父类的更匹配一些。现在修改一下子类代码,更改为:

public class Child extends Base {
    public long sum(int a, long b){
        System.out.println("child_int_long");
        return a+b;
    }
}

程序输出变为了:
child_int_long

终于调用了子类的方法。可以看出,当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。

父子类型转换
之前我们说过,子类型的对象可以赋值给父类型的引用变量,这叫向上转型,那父类型的变量可以赋值给子类型的变量吗?或者说可以向下转型吗?语法上可以进行强制类型转换,但不一定能转换成功。我们以上面的例子来示例:

Base b = new Child();
Child c = (Child)b;

Child c = (Child)b就是将变量b的类型强制转换为Child并赋值为c,这是没有问题的,因为b的动态类型就是Child,但下面代码是不行的:

Base b = new Base();
Child c = (Child)b;

语法上Java不会报错,但运行时会抛出错误,错误为类型转换异常。

一个父类的变量,能不能转换为一个子类的变量,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。

给定一个父类的变量,能不能知道它到底是不是某个子类的对象,从而安全的进行类型转换呢?答案是可以,通过instanceof关键字,看下面代码:

public boolean canCast(Base b){
    return b instanceof Child;
}

这个函数返回Base类型变量是否可以转换为Child类型,instanceof前面是变量,后面是类,返回值是boolean值,表示变量引用的对象是不是该类或其子类的对象。

protected
变量和函数有public/private修饰符,public表示外部可以访问,private表示只能内部使用,还有一种可见性介于中间的修饰符protected,表示虽然不能被外部任意访问,但可被子类访问。另外,在Java中,protected还表示可被同一个包中的其他类访问,不管其他类是不是该类的子类,后续章节我们再讨论包。

我们来看个例子,这是基类代码:

public class Base {
    protected  int currentStep;
    
    protected void step1(){
    }
    
    protected void step2(){        
    }
    
    public void action(){
        this.currentStep = 1;
        step1();
        this.currentStep = 2;
        step2();
    }
}

action()表示对外提供的行为,内部有两个步骤step1()和step2(),使用currentStep变量表示当前进行到了哪个步骤,step1、step2和currentStep是protected的,子类一般不重写action,而只重写step1和step2,同时,子类可以直接访问currentStep查看进行到了哪一步。子类的代码是:

public class Child extends Base {
    protected void step1(){
        System.out.println("child step "
                +this.currentStep);
    }
    
    protected void step2(){    
        System.out.println("child step "
                +this.currentStep);
    }
}

使用Child的代码是:

public static void main(String[] args){
    Child c = new Child();
    c.action();
}

输出为:
child step 1
child step 2

基类定义了表示对外行为的方法action,并定义了可以被子类重写的两个步骤step1和step2,以及被子类查看的变量currentStep,子类通过重写protected方法step1和step2来修改对外的行为。

这种思路和设计在设计模式中被称之为模板方法,action方法就是一个模板方法,它定义了实现的模板,而具体实现则由子类提供。模板方法在很多框架中有广泛的应用,这是使用protected的一个常用场景。关于更多设计模式的内容我们暂不介绍。

可见性重写
重写方法时,一般并不会修改方法的可见性。但我们还是要说明一点,重写时,子类方法不能降低父类方法的可见性,不能降低是指,父类如果是public,则子类也必须是public,父类如果是protected,子类可以是protected,也可以是public,即子类可以升级父类方法的可见性但不能降低。如下所示:

基类代码为:

public class Base {
    protected void protect(){
    }
    
    public void open(){        
    }
}

子类代码为:

public class Child extends Base {
    //以下是不允许的的,会有编译错误
//    private void protect(){
//    }
    
    //以下是不允许的,会有编译错误
//    protected void open(){        
//    }
    
    public void protect(){        
    }
}

为什么要这样规定呢?继承反映的是"is-a"的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏"is-a"的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。

防止继承 (final)
上节我们提到继承是把双刃剑,具体原因我们后续章节解说,带来的影响就是,有的时候我们不希望父类方法被子类重写,有的时候甚至不希望类被继承,实现这个的方法就是final关键字。之前我们提过final可以修饰变量,这是final的另一个用法。

一个Java类,默认情况下都是可以被继承的,但加了final关键字之后就不能被继承了,如下所示:

public final class Base {
   //.... 
}

一个非final的类,其中的public/protected实例方法默认情况下都是可以被重写的,但加了final关键字后就不能被重写了,如下所示:

public class Base {
    public final void test(){
        System.out.println("不能被重写");
    }
}

小结
本节我们讨论了Java继承概念引入的一些细节,有些细节可能平时遇到的比较少,但我们还是需要对它们有一个比较好的了解,包括构造方法的一些细节,变量和方法的重名,父子类型转换,protected,可见性重写,final等。

#三.继承是把双刃剑
继承其实是把双刃剑:一方面继承是非常强大的,另一方面是因为继承的破坏力也是很强的。

继承被广泛应用于各种Java API、框架和类库之中,一方面它们内部大量使用继承,另一方面,它们设计了良好的框架结构,提供了大量基类和基础公共代码。使用者可以使用继承,重写适当方法进行定制,就可以简单方便的实现强大的功能。

但,继承为什么会有破坏力呢?主要是因为继承可能破坏封装,而封装可以说是程序设计的第一原则 ;另一方面,继承可能没有反映出"is-a"关系。下面我们详细来说明。

1.继承破坏封装
什么是封装呢?封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。函数是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。可以说,封装是程序设计的第一原则,没有封装,代码之间到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的。

继承可能破坏封装是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。我们通过一些例子来说明。这些例子主要用于演示,可以基本忽略其实际意义。

2.封装是如何被破坏的
我们来看一个简单的例子,这是基类代码:

public class BaseV1 {
    private static final int   MAX_NUM = 1000;
    private              int[] arr     = new int[MAX_NUM];
    private int count;

    public void add(int number) {
        if (count < MAX_NUM) {
            arr[count++] = number;
        }
    }

    public void addAll(int[] numbers) {
        for (int num : numbers) {
           add(num);
        }
    }
}

Base提供了两个方法add和addAll,将输入数字添加到内部数组中。对使用者来说,add和addAll就是能够添加数字,具体是怎么添加的,应该不用关心。

子类代码Child(ChildV1.class)如下:

public class ChildV1 extends BaseV1 {
	
	private long sum;

	@Override
	public void add(int number) {
		super.add(number);
		sum+=number;
	}

	@Override
	public void addAll(int[] numbers) {
		super.addAll(numbers);
	       for(int i=0;i<numbers.length;i++){
			sum+=numbers[i];
  	      }
	}
	
	public long getSum() {
		return sum;
	}
}

子类重写了基类的add和addAll方法,在添加数字的同时汇总数字,存储数字的和到实例变量sum中,并提供了方法getSum获取sum的值。

使用Child的代码如下所示(chapter_4Activity.class):

ChildV1 cv1=new ChildV1();
cv1.addAll(new int[]{1,2,3});
Log.e(TAG, "继承是把双刃剑....... " + cv1.getSum());

使用addAll添加1,2,3,期望的输出是1+2+3=6,实际输出为12!为什么是12呢?查看代码不难看出,同一个数字被汇总了两次。子类的addAll方法首先调用了父类的addAll方法,而父类的addAll方法通过add方法添加,由于动态绑定,子类的add方法会执行,子类的add也会做汇总操作。

可以看出,如果子类不知道基类方法的实现细节,它就不能正确的进行扩展。知道了错误,现在我们修改子类实现,修改addAll方法为:

@Override
public void addAll(int[] numbers) {
    super.addAll(numbers);
}

也就是说,addAll方法不再进行重复汇总。这下,程序就可以输出正确结果6了。

但是,基类Base决定修改addAll方法的实现,改为下面代码:

public void addAll(int[] numbers){
    for(int num : numbers){
        if(count<MAX_NUM){
            arr[count++] = num;    
        }
    }
}

也就是说,它不再通过调用add方法添加,这是Base类的实现细节。但是,修改了基类的内部细节后,上面使用子类的程序却错了,输出由正确值6变为了0。

从这个例子,可以看出,子类和父类之间是细节依赖,子类扩展父类,仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类

更具体地说,子类需要知道父类的可重写方法之间的依赖关系,具体到上例中,就是add和addAll方法之间的关系,而且这个依赖关系,父类不能随意改变

即使这个依赖关系不变,封装还是可能被破坏。还是以上面的例子,我们先将addAll方法改回去,这次,我们在基类Base中添加一个方法clear,这个方法的作用是将所有添加的数字清空,代码如下:

public void clear(){
    for(int i=0;i<count;i++){
        arr[i]=0;
    }
    count = 0;
}

基类添加一个方法不需要告诉子类,Child类不知道Base类添加了这么一个方法,但因为继承关系,Child类却自动拥有了这么一个方法!因此,Child类的使用者可能会这么使用Child类:

    Child c = new Child();
    c.addAll(new int[]{1,2,3});
    c.clear();
    c.addAll(new int[]{1,2,3});
    System.out.println(c.getSum());

先添加一次,之后调用clear清空,又添加一次,最后输出sum,期望结果是6,但实际输出呢?是12。为什么呢?因为Child没有重写clear方法,它需要增加如下代码,重置其内部的sum值:

@Override
public void clear() {
    super.clear();
    this.sum = 0;
}

可以看出,父类不能随意增加公开方法,因为给父类增加就是给所有子类增加,而子类可能必须要重写该方法才能确保方法的正确性

总结一下:对于子类而言,通过继承实现,是没有安全保障的,父类修改内部实现细节,它的功能就可能会被破坏,而对于基类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。

3.继承没有反映 is-a 关系
继承关系是被设计用来反映 is-a 关系的,子类是父类的一种,子类对象也属于父类,父类的属性和行为也一定适用于子类。就像橙子是水果一样,水果有的属性和行为,橙子也必然都有。

但现实中,设计完全符合 is-a 关系的继承关系是困难的。比如说,绝大部分鸟都会飞,可能就想给鸟类增加一个方法fly()表示飞,但有一些鸟就不会飞,比如说企鹅。

在is-a关系中,重写方法时,子类不应该改变父类预期的行为,但是这是没有办法约束的。比如说,还是以鸟为例,你可能给父类增加了fly()方法,对企鹅,你可能想,企鹅不会飞,但可以走和游泳,就在企鹅的fly()方法中,实现了有关走或游泳的逻辑。

继承是应该被当做 is-a关系使用的,但是,Java并没有办法约束,父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。

但通过父类引用操作子类对象的程序而言,它是把对象当做父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,结果是什么呢?混乱。

4.如何应对继承的双面性?
继承既强大又有破坏性,那怎么办呢?
1)避免使用继承
2)正确使用继承

我们先来看怎么避免继承,有三种方法:

  • 使用final关键字
  • 优先使用组合而非继承
  • 使用接口

(1)使用final避免继承
在上节,我们提到过final类和final方法,final方法不能被重写,final类不能被继承,我们没有解释为什么需要它们。通过上面的介绍,我们就应该能够理解其中的一些原因了。

给方法加final修饰符,父类就保留了随意修改这个方法内部实现的自由,使用这个方法的程序也可以确保其行为是符合父类声明的。

给类加final修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心的使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。

(2)优先使用组合而非继承
使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该被优先使用。还是上面的例子,我们使用组合来重写一下子类,代码如下(ChildV2.class):

public class ChildV2 {

    private BaseV1 mBaseV1;

    private long sum;

    public ChildV2() {
        mBaseV1 = new BaseV1();
    }

    public void add(int number) {
        mBaseV1.add(number);
        sum += number;
    }


    public void addAll(int[] numbers) {
        mBaseV1.addAll(numbers);
        for (int i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }

    public long getSum() {
        return sum;
    }
}

这样,子类就不需要关注基类是如何实现的了,基类修改实现细节,增加公开方法,也不会影响到子类了。但组合的问题是,子类对象不能被当做基类对象,被统一处理了。解决方法是使用接口

(3) the proper use of inheritance
if you want to use inheritance, how to use it properly? Probably using inheritance There are three main scenarios:
1) the base class is written by someone else, we write subclasses.
2) We write the base class, others may write subclasses.
3) the base class, subclass is we write.

The first scene, the base class is mainly Java API, or other framework class library, in this case, we mainly by extending the base class to implement custom behavior, in this case should be noted that:

  • Overriding methods do not change the expected behavior.
  • Read the documentation, understand rewritable method of implementation mechanism, in particular the relationship between method calls.
  • In the case where the base class changes, modifications read the specification, the corresponding modified subclasses.

The second scene, we write the base class for others to use, in this case, it should be noted that:

  • Use reflect the true inheritance is-a relationship, only the real part into common base class.
  • I do not want to expose method is overridden to add the final modifier.
  • Written document, the implementation mechanism can override methods to provide guidance for the sub-category, sub-category tells how to rewrite.
    When the base class modifications that may affect the subclass, modify write instructions.

A third scenario, we both write base classes, subclasses also wrote, with regard to the base class, notes and similar to the second scene, on the subclass, notes and similar to the first scene, but we control procedures by requiring you can properly relax.

4. Summary
above us about why the inheritance is double-edged sword, though powerful inheritance, but inheritance may damage the package and the package can be said that the first principle of programming, inheritance can also be misused, does not reflect the real is-a relationship.

We also explains how to deal with the succession of double-sided, on the one hand to avoid inheritance, avoid using the final, using a combination of priority, using the interface. If you want to use inheritance, we also introduced the precautions in the use of inheritance of three scenarios.

Published 81 original articles · won praise 37 · views 50000 +

Guess you like

Origin blog.csdn.net/gaolh89/article/details/95914533