--- 상속 객체 지향 클래스 : 자바 기반

마지막 장에서는, 우리는 프로그램의 개념의 현실을 매핑의 개념은, 우리가되는 매우 중요한 관계가 현실의 개념 사이에, 클래스와 클래스 사이의 조합에 대해 이야기하는 방법에 대해 이야기 분류가 . 다음 분류 뿌리, 그리고 계층 적 분류 시스템을 형성하기 위해 아래로 계속 수정. 이 예는 매우이다 :

1) 자연의 세계에서,이 생물 동물과 식물, 동물은 다른 과목, 육식, 초식 동물, 잡식 동물 한하고 다른 육식 동물은 다른 종류로 나누어집니다 등 늑대, 개, 호랑이,이다.

2) 가전 제품, 의류 등의 눈에 잘 띄는 위치에 일반적인 분류 목록에서 전기 공급 업체 사이트를 열고, 의류는 남성, 여성,있다, 남자 셔츠, 청바지 등등이 ...

컴퓨터 프로그램들은 클래스 사이에 사용되는 상속 객체 관계의 분류와의 관계를 나타내는. 계층 구조에서,이 부모 클래스서브 클래스 같은 개, 동물 동물 개로서, 동물은 부모 클래스는, 개는 서브 클래스입니다. 또한 상위라는 기본 클래스 라고도, 서브 클래스, 파생 클래스 , 하위 클래스는 상위 클래스 반대 클래스 B는 클래스 A의 서브 클래스 일 수 있고, 클래스 C는 상위 클래스이다.

서브 클래스는 속성과 부모의 행동을 상속하기 때문에 그 이유는, 상속이라고, 일부 서브 클래스의 상위 클래스의 속성과 동작이 있습니다 . 그러나 서브 클래스가 서브 클래스 고유의 속성과 행동을 증가시킬 수 있습니다 , 일부 부모 클래스, 어떤 행동을, 구현 서브 클래스는 부모 클래스와 정확히 일치하지 않을 수 있습니다.

코드를 재사용 할 수있는 한편 상속을 사용하여 공용 속성과 행동은 부모 클래스에 배치하고,에 초점을 서브 클래스 고유의 서브 클래스만을 필요로 할 수 있으며, 다른 한편으로는, 다른 서브 클래스의 객체가 더 편리 할 수 ​​있습니다 통합 치료.

이 자세한 연구는 상속을 요구했다. 우리가 처음 상속, 승계의 기본 개념을 소개하고 사용의 상속을 이해, 세부 사항의 일부를 설명 후, 우리는 상속 고려 사항에 대해 얘기 상속은 양날의 칼, 어떻게 제대로 상속을 사용하는 이유를 설명한다.

# A. 기본 개념
## 1. 루트 클래스 Object

자바에서 모든 클래스는 부모가 선언하지 않는 경우에도, 암시 적 부모 개체라는 부모 클래스가, 부모가있다. 아래 그림과 같이 개체 속성을 정의하지만, 방법의 숫자의 정의되지 않습니다
쓰기 사진은 여기에 설명

이 섹션에서 우리는 그렇지 않으면 우리는 점차적으로 다음 섹션에서 소개합니다, toString () 메소드를 소개합니다. 목적 toString () 메서드는이 방법의 텍스트 설명이 직접 모든 클래스에 사용할 수있는 개체를 반환합니다.

당신이 toString 메소드를 사용할 수 있도록 예를 들어, Point 클래스를 위해 우리는 앞서 소개 :

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

이 같은 출력 :

Point@76f9aa66

그것은 무엇을 의미 하는가? 에서 @ 전에 @ 무엇입니까 후 클래스 이름은? 우리는의 toString 코드를 보면 :

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

getClass (). getName ()가 현재 객체의 클래스 이름을 반환 , 해시 코드는 () 객체의 해시 값, 우리는 다음 장에서 소개 해시를 반환, 여기를 정수로 이해 될 수있다 기본적으로, 일반적으로 객체의 메모리 어드레스 값, 해시 값의는, Integer.toHexString (해시 코드 ()) 복귀 shiliu 16 진수 표현.

왜 당신은 그것을 쓴? 클래스 이름은 이해할 객체의 유형을 나타내며, Object 클래스는, 특정 객체의 속성을 모르는 텍스트 설명을 사용하는 방법을 모르기 때문에 쓰기 해시 값이 최후의 수단이지만, 필요 만 할 수있는, 다른 개체를 구분하는 쓰기 해시 값을 작성합니다.

그러나 서브 클래스는 서브 클래스 알려진 자신의 속성이 있습니다 재정의 서로 다른 구현을 반영하기 위해 부모 방법. 소위 재 작성, 부모 클래스 정의 및 방법, 그리고 다시 구현과 동일합니다.

## (2)에있어서의 겹쳐

하나, 우리는 Point 클래스를 포함하여 그래픽 카테고리의 일부, 우리는 toString () 메서드를 다시 이번에 소개합니다. 아래 코드 (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하는 전방 보유 @Override 이 방법 toString 것은 나타내고, 상위 클래스에 재 기입하는 방법이 포인트 X와 Y 좌표를 덮어 후, 메소드 리턴 값 (코스를, 부모의 부모 클래스 일 수있다). 통화 구현 서브 클래스를 재 작성 후. 예를 들어, 다음의 코드가 출력된다 (2,3)

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

## 3 그래픽 클래스 상속 시스템

다음으로, 상기 도면에서의 그래픽 처리, 제 모습의 일부의 예를 설명 :

쓰기 사진은 여기에 설명

이러한 기본 패턴 등 유선 모양, 사각형, 삼각형, 원형, 일부, 색이 다른 패턴을 갖는다. 다음으로, 우리는 상속의 개념을 설명하기 위해 다음과 같은 클래스를 정의 :

  • 상위 클래스 모양, 그래픽 표현입니다.
  • 클래스 원은 원을 나타냅니다.
  • 클래스 라인, 직선을 나타냅니다.
  • 클래스 ArrowLine, 화살표는 선형을 나타내며으로

1. 그래픽
모든 그래픽 (도형)가 속성에 의해 표시되는 색상을 가지고 다음과 같은 도면의 표현 (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");
	}
}

상기 코드는 기본적으로 어떠한 해석, 인스턴스 변수 색 우리가 주로 상속을 설명하기 위해, 실제 렌더링 코드를 작성하지 않을 방법은 연신을 나타낸다 그릴 색상을 표시하지 않는다.

2. 원
원 (원)이 형상으로부터 상속 만, 면적 계산에 사용되는 영역 이외에, 다음과 같은 코드로 다시 연신 추가적인 특성 중심점과 반경뿐만 아니라 부가적인 방법을 포함한다 :

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;
    }

}

설명 :

  1. 자바는 사용 확장 , 클래스는 최대 하나의 부모를 가질 수, 키워드 표시된 상속을
    2) 직접 액세스 할 개인 특성과 부모 클래스의 방법, 예를 들어, 원에, 당신이 직접 개인 인스턴스 변수 색상 모양에 액세스 할 수없는 수의 서브 클래스;
    3) 개인 이외에 서브 클래스 상속 방법 및 상위 클래스의 다른 특성, 예를 들어, 연신 법 원에서 직접 getColor에서 () 메소드를 호출 할 수있다.

의 검증 보자. 아래 코드 (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());

프로그램의 출력은 다음과 같습니다

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

여기에 다소 이상한, 색이 될 때 할당됩니다? 새로운 방법에서, 상위 클래스 생성자는 제 구현을 수행하고, 우선 서브한다. 이 예에서, 슈퍼 클래스 모양 생성자의 기본 생성자는 원형의 서브 클래스하기 전에 실행됩니다. 새로운 프로세스에 대한 세부 사항, 우리는 더 다음 절에서 설명한다.

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) 상속의 적절한 사용
을 사용 상속을 원하는 경우, 어떻게 그것을 제대로 사용하는 방법? 아마 상속 사용하여 세 가지 주요 시나리오가 있습니다 :
1) 기본 클래스가 다른 사람에 의해 작성된 것입니다, 우리는 서브 클래스를 작성합니다.
2) 우리는 기본 클래스를 작성, 다른 서브 클래스를 쓸 수 있습니다.
3), 서브 클래스는 우리가 작성하는 기본 클래스입니다.

첫 번째 장면은, 기본 클래스는 주목해야한다, 우리는 주로이 경우, 사용자 정의 동작을 구현하는 기본 클래스를 확장하여 주로이 경우 자바 API, 또는 다른 프레임 워크 클래스 라이브러리입니다 :

  • 재정의 방법은 예상되는 동작을 변경하지 마십시오.
  • 설명서를 읽고 구현 메커니즘의 재기록 방법, 메소드 호출 사이에 특정의 관계를 이해한다.
  • 베이스 클래스가 변경 될 경우, 변경은 본 명세서의 해당 서브 변형을 읽었다.

두 번째 장면, 우리가 사용하는 다른 사람에 대한 기본 클래스를 작성,이 경우, 그것은 주목해야한다 :

  • 사용은 진정한 유산은-관계, 공통 기본 클래스에 단지 실수 부분 반영합니다.
  • 나는 최종 수정을 추가 오버라이드 (override) 방법을 노출하고 싶지 않아요.
  • 작성된 문서는 하위 범주에 대한 지침을 제공하는 방법을 재정의 할 수 있습니다 구현 메커니즘은 하위 범주는 다시 작성하는 방법에 대해 설명합니다.
    때 쓰기 지침을 수정, 서브 클래스에 영향을 미칠 수있는 기본 클래스 수정.

세 번째 시나리오, 우리 모두 쓰기 기본 클래스, 서브 클래스는 서브 클래스, 노트와 첫 번째 장면과 유사의 기본 클래스, 메모에 관한 두 번째 장면과 유사하여, 썼다, 그러나 우리는 필요에 의한 절차를 제어 제대로 휴식을 취할 수 있습니다.

4. 요약
상속 강력한 상속하지만, 양날의 검이지만, 상속 패키지를 손상시킬 수 있으며, 패키지는 프로그램의 첫 번째 원칙은, 상속도 오용 될 수 있다고 할 수있는 이유에 대한 우리 위는 실제를 반영하지 않는 것은-A 관계.

우리는 또한 승계를 처리하는 방법에 대해 설명합니다 인터페이스를 사용하여, 우선 순위의 조합을 사용하여, 최종 사용을 피 상속, 피할 수있는 한편, 양면. 상속을 사용하려는 경우, 우리는 세 가지 시나리오 상속의 사용에주의 사항을 소개했다.

게시 81 개 원래 기사 · 원 찬양 37 ·은 50000 +를 볼

추천

출처blog.csdn.net/gaolh89/article/details/95914533