java类的继承

目录

  • 如何实现继承
  • 覆盖成员变量
  • 重写父类的方法
    • 重写与private
    • 重写与构造器
    • 重写与static
    • 重写与@override注解
    • super与父类构造器
    • super与父类实例成员
  • 多态
    • 多态的实现原理
    • 多态与强制类型转换
      • instanceof运算符
  • 继承与破坏封装
  • 何时使用继承
  • 继承与组合
    • 何时用组合



继承更应该叫做扩展,关键字是extends(扩展的意思)。
实现继承的类叫子类派生类,被继承的类叫做父类、基类、超类
子类与父类是特殊与一般的关系,父类有更大的范围,子类属于父类。
子类会获得父类的成员变量、成员方法、内部类(子类无法继承创建父类部分的代码:静态初始化块、初始化块、构造器都不能继承得到)。并且在此基础上,进一步增加自己的成员变量和方法
继承的成员变量、方法和内部类包括静态和非静态成员,类变量、类方法、静态内部类都会被子类继承。
父类的成员变量和方法相当于所有子类都有的特点,这决定了所有子类都属于父类,而子类自身增加的部分就是每个子类独有的。
java的继承是单继承关系,每个子类只能有一个直接父类(但可以有多个间接父类)。
java不支持多继承,但是可以有多层继承,每一层都是单继承。



如何实现继承

实现继承的语法非常简单,就是在子类类名后增加extends 父类名
比如:public class Apple extends Fruit {类内部省略}



覆盖成员变量

如果子类的成员变量与父类同名,那么子类成员变量会覆盖掉父类的同名变量
这种覆盖只需要考虑同名,不需要考虑类型是否相同,修饰符是否相同
如果父类的成员变量是int类型,子类同名变量是String类型,同名的父类变量也会被覆盖。
与修饰符也无关,即使父类的变量是类变量,子类的同名变量是实例变量,也会被覆盖。

这是因为变量名是变量的标识,系统是根据变量名去寻找对应变量的存储空间。只要变量名相同,系统就只会优先访问子类的同名变量。

如果想访问父类的同名变量
1.在子类内部必须使用supr关键字(同名的实例变量)或者父类类名(同名的类变量)。
2.在子类外部必须使用强制类型转换,将子类的引用强制转换为父类的引用。
语法为
((父类类名)子类引用变量名).同名变量名
当然,这种直接访问成员变量的方式破坏了封装,所以第二种方式是不建议使用的。
一般情况还是通过设计专门访问父类对应同名变量的公开方法,再在方法内部用第一种方式访问父类同名变量。

如果父类的方法调用了父类的成员变量,同时子类也有同名的成员变量。子类继承下这个方法后,不会调用子类的同名成员变量,而是依旧调用父类的成员变量
举例:

public class CoverBase {
    protected int sameName = 100;

    public int getSameName() {return sameName;};
}

class CoverSub extends CoverBase{
    public int sameName = 1000;

    public static void main(String[] args) {
        CoverSub cs = new CoverSub();
        System.out.println(cs.getSameName());
    }
}

输出的结果是100,而不是1000。因为子类继承父类的方法,就在父类方法内执行,调用的变量自然是父类的变量,即使子类有同名的变量,也不会覆盖。只有在子类的方法中调用该同名变量,才会被覆盖。

总结就是:在方法中访问实例变量,如果是继承自父类的方法,就访问父类的实例变量,如果是子类的方法,就访问子类的实例变量



重写父类的方法

子类除了增加新的成员变量和方法外,还可能会重写父类的方法
比如:鸵鸟是鸟的子类,但是鸵鸟不能飞翔,所以鸵鸟的fly方法需要重写鸟类的fly方法。

子类重写方法有“两同两小一大”原则。两同是方法名相同、参数列表相同。两小是声明抛出的异常类更小或者相等,子类返回值类型比父类更小或者相等。一大是子类重写方法的访问权限比父类方法更大或者相等。严格说应该还有一同,就是子类重写的方法和父类方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。

重写与private

如果父类的成员变量或者方法具有private访问权限,则这些方法和成员变量虽然被分配了内存空间,但对子类也是隐藏的。
即使子类有和父类private方法同名,同参数列表的方法,满足“两同两小一大”的要求,也不算重写。只能算子类增加了一个新的方法。
对于父类的private成员变量,子类虽然没办法直接访问和修改,但是可以他通过父类提供的公有setter和getter方法,实现访问和修改

重写与构造器

父类构造器不会被子类继承,更不会被重写,父类构造器只能被子类构造器在初始化阶段调用。

重写与static

重写是对象相关的,所以子类无法重写父类的类方法,如果子类满足重写条件的同名类方法,只是定义了新方法。
但是很奇怪的是,如果子类要定义和父类方法签名相同的类方法,必须满足两同两小一大的规则。虽然java认为这不算方法重写,但为了访问不出现冲突,还是得满足与重写相同的规则。



重写与@override注解

可以在子类重写的方法上一行加上@override的注解,这样java会帮你做类型检测,如果重写的方法不满足规则,或者父类的方法本身不能被重写,就会出现编译错误。



# super关键字 **super的含义是子类对象对应的那个直接父类对象**,和this关键字的含义很像。

super与父类构造器

子类不会获得父类的构造器,但是子类构造器会调用父类的构造器。
实际上,父类对象的构造器一定会先于子类的构造器执行,最先执行的是对应的Object对象的构造器,Object类是所有类的父类。
所以,在执行子类对象初始化代码之前,一定优先执行父类的初始化代码。
父类的构造器代码一定先于子类构造器代码执行

有一个很有趣的规定:super(参数列表) 和 this(参数列表),这两个语法一个用于调用父类构造器,一个用于调用子类自身重载的另一个构造器。它们都规定:如果出现在构造器代码内部,必须是第一行
刚开始会觉得很费解,仔细思考才能理解:
核心原因是父类对象的创建一定先于子类对象的创建。但是,子类可能会执行多个重载构造器的代码
排序类似下面:
父类构造器1,2,3 -> 子类构造器1,2,3
这里,父类构造器3执行后,紧接着要执行的是子类构造器1。
子类构造器1想要调用父类构造器3,就必须使用super关键字(即使子类构造器1里没有显式使用super关键字,系统也会隐式的调用super(); 调用父类的无参构造器)。
这就解释了为什么super(形参列表)出现在构造器内时,必须是第一行代码。因为系统要保证在执行第一个子类构造器之前,必须通过super执行父类构造器的代码
而为什么this(参数列表)出现在构造器内时,也要是第一行代码。这是因为使用this的目的是为了先调用重载的构造器,这说明这个构造器不是子类最先执行的构造器,至少优先调用的重载构造器比当前使用this的构造器更早执行。所以,使用this的构造器不需要使用super关键字,也就不会产生矛盾。

如果父类没有提供无参构造器,那么子类第一个执行的构造器必须显式调用父类的某个有参构造器

super与父类实例成员

子类继承得到的父类成员变量和方法会被存储在单独的内存空间注意:哪怕子类有同名的成员变量和方法,也不会覆盖父类成员的内存区域,只是如果没显示指定调用者的话,会优先访问子类的同名成员变量和方法,可以称为父类的实例成员被隐藏了

父类的类变量和类方法如果已经被同名的子类类变量和类方法覆盖,就需要显式用父类类名.类变量父类类名.类方法(形参列表)访问。
父类的实例变量和实例方法如果被覆盖,就需要用super.方法名(形参列表)和super.实例变量显式访问。

和this关键字一样,super是与对象相关的,所以super不能出现在类方法中



多态

多态的本质是:同一个行为具有多种表现形式或形态

多态的核心语法是:引用变量的类型是指向对象类型的父类或者父接口
虽然引用变量和对象类型不完全一样,但是它们存在特殊和一般的关系。
子类对象相当于特殊的父类,实现类相当于实现了接口定义规则的一个具体例子。我们把这种操作称为向上转型(upcasting)。

多态的实现原理

多态的表现和成员变量无关与方法直接相关
引用变量的类型决定了能访问的方法的范围(子类或者实现类新增的方法无法被访问),但方法具体的表现由指向的对象决定。

java的引用变量有两个类型:编译时类型和运行时类型
编译时类型由声明引用变量时的类型决定,运行时类型由指向的对象类型决定

在编译阶段,引用变量只能调用其编译时类型具有的方法,也就是父类或者父接口的方法,子类或者实现类新增的方法无法被调用。
在运行阶段,真正执行方法代码时,引用变量会执行运行时阶段的实例方法代码,也就是子类或实现类对象的实例方法代码。
在运行阶段时,系统在真正执行方法时,会先查找子类或实现类的内存区域是否有该实例方法,如果没有,再查找直接父类的内存区域是否有该实例方法(如果是接口与实现类,就查找直接父接口的默认方法),依次类推,查找到Object类或者最上层父接口是否有该实例方法。因为编译阶段已经确定该实例方法存在,所以一定能找到。按这种逻辑找,如果子类已经重写了父类或父接口的实例方法,那一定会执行子类重写的同名实例方法

比如你定义了一个Animal的父类,有Dog和Cat两个子类,Dog和Cat的makeNoise方法是不一样的,Animal本身可能也有一个makeNoise方法。也就是Dog和Cat都重写了Animal的makeNoise方法。
如果你的引用变量是Animal类型,如果它指向的对象是Dog类型的子类对象,那它执行makeNoise方法会执行Dog的makeNoise方法。如果它指向的对象时Cat类型的子类对象,那它执行makeNoise方法会执行Cat的makeNoise方法。

注意:如果父类(接口)引用变量调用的是类方法,其实与多态无关。因为类方法不能被重写,不具备多态的性质。只是java有一个不好的设计,可以使用引用变量名.类方法的形式调用类方法,底层会被替换为类名(接口名).类方法的形式。由于引用变量是父类或者父接口类型,所以会调用父类或者父接口的类方法。

这种相同类型的引用变量,调用同一个方法,可能呈现多种不同的行为特征(因为指向的子类对象不一样),我们称为多态

但是,在访问成员变量时,引用变量会在编译阶段就访问成员变量,那自然是父类的成员变量。所以多态与成员变量无关

多态与强制类型转换

如果不强制类型转换,父类引用变量是没办法运行子类新增加的方法的,即使引用的子类对象存在该方法。
想要执行子类新增加的方法,只能使用强制类型转换符
语法为:(type)variable

对于父接口和实现类的原理一样,就不赘述了。

强制类型转换不仅可以对基本数据类型的变量进行类型转换,还可以在父类子类类型之间进行
但是,没有继承关系的两个引用类型,是没办法进行强制类型转化的(强行执行会出现编译错误)。

子类类型转换为父类是一定可以的,毕竟子类就是特殊的父类

父类类型的引用转换为子类类型的前提是父类引用变量指向对应子类的对象。否则会引发异常。

总结就是:强制类型转换引用类型必须具有继承关系,而且父类引用转换为子类引用的前提是,引用变量指向对应子类对象

instanceof运算符

在进行强制类型转换时,最好使用instanceof运算符做判断,为true时才进行转换。
这样可以提高程序的健壮性。
A instanceof B的中文意思是A指向的对象是类B的一个实例
A一般是一个引用类型变量,B通常是一个类。
也就是引用变量A指向的对象是类B的实例注意:我们判断是看引用变量A指向的对象的类型,而不是引用变量的类型

引用变量A和类B的类型本身必须具有父子继承关系,否则判断本身就毫无意义,会直接引发编译错误

如果引用变量A指向的对象是B类的父类,或者引用变量A指向的对象和B类拥有共同的父类,但它们本身是不同的子类,表达式结果为false

如果引用变量A指向的对象和B是相同类型,或者是B的子类,表达式结果为true
为什么引用变量A指向的对象是B的子类,结果为true呢?这是因为子类就是特殊的父类,子类对象自然是父类的实例。

用代码举例说明:

String world = "world";
// 下面代码无法通过编译,因为world是String类型,和Math类型没有继承关系
//System.out.println(world instanceof Math);

// 下面代码无法通过编译,虽然引用类型o是Object的子类,但是o并没有初始化
// 因为我们判断的是看引用变量o指向的对象的类型与Object的关系,o没有指向任何对象,所以无法通过编译
String o;
//System.out.println(o instanceof Object);


// 下面是false的情况
// 这就是多态的用法,hello引用是Object类型,但是它指向的对象是Object的子类,String类型
Object hello = "hello";
//虽然hello引用的对象是String类型,但是hello引用变量的类型是Object,和Math有继承关系,所以有判断的必要
// hello引用的对象是String类型,和Math类型是不同类型的子类,所以结果为false
System.out.println(hello instanceof Math);	

// object是Object类型
Object object = new Object();
//object指向的对象是Object类型,是String类型的父类,所以结果是false
System.out.println(object instanceof  String);

// 下面是true的情况
// hello引用的对象是String类型,是Object类型的子类,和String是相同类型,所以下面两个是true
System.out.println(hello instanceof Object);
System.out.println(hello instanceof String);


继承与破坏封装

子类可以继承父类的成员变量和方法,但是这也导致子类和父类的严重耦合
子类可以访问父类的成员变量和方法(非private),还可以重写父类的方法,改变实现功能的细节。这些严重破坏了父类的封装。

为了保证父类的封装性,设计父类要遵循如下规则

  1. 父类所有的成员变量都设置为private访问类型,不要让子类直接访问父类的成员变量。
  2. 父类的工具方法都设置为private,父类希望被外界访问,但不希望被重写的方法加上final修饰符,父类希望被子类重写但不希望被外界访问的方法设置为protected访问权限。
  3. 不要在父类构造器中调用将要被子类重写的方法。

关于第三点是因为,如果父类构造器调用将被子类重写的方法,那在创建子类时,会先执行父类的构造器,此时父类构造器就会调用子类已经重写后的方法,这个方法是和子类绑定的,可能会和子类新添加的成员交互,父类根本没这些成员,就可能引发异常。



何时使用继承

派生子类需要满足以下条件之一

  1. 子类需要额外增加成员变量。
  2. 子类需要增加自己独有的行为(包括增加新的方法或者重写父类方法)。

很多时候是不需要继承的,比如Animal类,没必要设计BigAnimal和smallAnimal,只需要增加一个size的实例变量。

但是,狗和猫就需要继承Animal,你不能简单的增加一个species(物种)的实例变量。你虽然可以让species的值等于dog或者cat来区分狗和猫,但是狗和猫有自己独立的行为,狗的叫声和猫的叫声是截然不同的。你不可能设计一个makeNoise的方法,传入species的参数,根据参数的值是狗还是猫决定执行什么叫声,这不是好的设计。
更好的设计就是使用继承,让狗和猫自己重写Animal的makeNoise的方法。



继承与组合

组合是把旧类当做新类的成员变量,从而实现在内部访问旧类的方法。
组合和继承都可以实现代码的复用。
但是组合相比继承有更好的封装性,因为组合只能访问旧类公开的方法,没有破坏封装
举例:人与老师,人与学生,这是继承;人与手臂,人与腿,这是组合。
人拥有手臂这个对象,手臂是人的实例变量,人可以调用手臂的方法执行手臂允许的动作。这也实现了复用。

何时用组合

如果两个类有明确的整体与部分关系,就应该使用组合
比如手臂是人的组成部分。
可以用(has-a)来检测,比如人有一条手臂,检测通过。
继承应该用(is-a)来检测,比如老师是一个人,检测通过。

你可能感兴趣的:(javaSE,java,开发语言)