[01] 继承


1、继承的声明

继承,是指一个类的定义可以基于另一个已经存在的类,即子类基于父类,从而实现父类代码的重用,子类能吸收父类的属性和行为,并扩展新的能力。Java中的继承是单继承,即最多只能有一个父类。

所谓 “龙生龙,凤生凤,老鼠的儿子会打洞”,这句话简单明白地阐述了继承:
  • 子类基于父类,也意味着是 “is-a” 的关系
  • 子类拥有父类的能力,也就是代码得到了复用

继承的声明也很简单,直接使用extends关键字即可:
【访问权限修饰符】【修饰符】子类名 extends 父类名 { 子类类体 } 



2、构造方法

子类能复用的是父类的属性和方法,但是这里并不包括构造方法,因为构造方法是不能被继承的。但是可以在子类构造方法中通过super关键字调用父类构造方法,super只能在构造方法的首行。
//父类
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }
}

//子类
public class Cat extends Animal{
    public Cat(String name) {
        super(name);
    }
}

如果你不想调用父类的构造函数,那么也必须保证父类有一个无参的构造函数,子类在调用自身构造函数时会默认调用父类无参构造函数(相当于默认在构造方法首行插入super( )方法)。

也即使是说,无论如何,要保证父类的构造函数,因为子类的构造方法总是先调用父类的构造方法,再调用自己的构造方法。毕竟,没有老子哪来的儿子?



3、this和super

this关键字代表自身,主要用在:
  • 在构造方法中引用自身类的其他构造方法
  • 用this代表自身类的对象(例如用this引用成员变量或方法)
public class Animal {

    private String name;
    private int age;

    public Animal(String name) {
        this.name = name;
    }

    public Animal(String name, int age) {
        this(name);
        this.age = age;
    }

    public String run() {
        return "run";
    }

    public String runQuickly() {
        return this.age + " " + this.name + " " + this.run() + " quickly";
    }

    public static void main(String[] args) {
        Animal animal = new Animal("diudiu", 12);
        System.out.println(animal.run()); //输出 run
        System.out.println(animal.runQuickly()); //输出 12 diudiu run quickly
    }

}

super和this类似,但是super是只有在继承关系里才会使用到的关键字,this是在本类中调用本类,而super则是在本类中调用父类:
  • 调用父类的构造方法(super语句只能在子类构造函数的第一行)
  • 调用父类的属性或方法
public class Cat extends Animal{

    public Cat(String name, int age) {
        super(name, age);
    }

    public String catRun() {
        return "cat " + super.runQuickly();
    }

    public static void main(String[] args) {
        Cat cat = new Cat("huahua", 9);
        System.out.println(cat.catRun()); //输出 cat 9 huahua run quickly
    }

}



4、private和继承

假如父类的某个属性权限设置为private,子类继承父类以后,却是无法直接调用该属性的,所以private的属性无法继承吗?

但是假如该属性有一个public的get方法,子类却可以通过get获取到当初无法直接访问的属性的值,所以private的属性实际上得到了继承吗?

实际上,继承的这个关系有点微妙,在我的理解看来,每次我们获取到一个子类对象时,实际上它包含了 “子类对象和其父类对象”,也就是说,每次我们实例化一个子类对象时,表面上看只有一个对象,实际上背后调用涉及的是一个子类加父类的组合。

我们看看子类实例化的过程和顺序是这样的:
  • 初始化父类的静态代码
  • 初始化子类的静态代码
  • 初始化父类的非静态代码
  • 初始化父类构造函数
  • 初始化子类非静态代码
  • 初始化子类构造函数

但是两者并不是相互交融的,域还是各自的域,但相互又有些关联,所以所谓继承下来的东西,只是让我们看起来像子类的了。而实际上是,当调用子类对象的方法或属性时,先看子类是否有,如果没有,就到父类对象(且权限允许的情况下)去调用。这也是为何子类与父类同名的属性和方法,可以将父类的覆盖掉的原因。

所以开始的private的那个问题也就可以理解了,private属性实际上也是有的,在对应的父类对象里,但是因为没有权限,所以无法直接访问。

最后一个示例,配合食用更佳:
//父类
public class Animal {

    public String name = "animal";

    public String getName() {
        return this.name;
    }
    
}

//子类
public class Cat extends Animal{

    public String name = "cat";

}

//测试类
public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        System.out.println(cat.name); //输出cat
        System.out.println(cat.getName()); //输出animal
    }
}



5、protected引发的权限修饰符认知纠偏

先来回顾一下权限访问修饰符的控制范围:
权限访问修饰符定义权限针对范围
public    公共权限    可以被任意类访问属性、方法、类
protected    受保护的权限同包类可以访问,或者非同包的该类子类可访问属性、方法
default(即默认不写)同包权限只能被同包的类访问属性、方法、类
private    私有权限    只能在本类中访问使用    属性、方法

一直以来我个人都错误地理解了权限修饰符,应该说,把如上表中的权限和针对范围部分混淆了。

在上个标题《4、private和继承》中已经从权限的一部分去说明了和继承的关系,当有一次用到protected权限时(注意看包位置):
//父类
package temp.animal;

public class Animal {
    public void eat() {
        System.out.println("Animal eat!");
    }
    protected void run() {
        System.out.println("Animal run!");
    }
}

//子类
package temp.animal;

public class Cat extends Animal{
}

//测试类
package temp.test;
import temp.animal.Cat;

public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat(); // compile ok
        cat.run(); // compile error
    }
}

Cat是Animal的子类,是继承关系。当我在一个测试类中,新建了一个Cat类对象,试图调用Cat类继承下来的protected修饰的方法run()时,编译不通过。

这里的变量cat,是Cat类吗?是的。Cat类是Animal的子类吗?是的。Cat类继承了Animal的protected方法run()吗?继承了。那为什么cat不能调用run()?

我说了,我的认知把权限和针对范围混淆了,这里的权限修饰符表示的权限始终是针对类的,也就是说,变量cat能否调用protected的run()方法,主要在于它在哪个类里,cat是在Test类中调用的,但是Test显然和Animal不同包,Test也不是Animal子类,所以是没有权限的,也就无法调用。相反,如果在Cat类中新建变量cat,就可以调用run(),或者Test和Animal同包,也是可以调用run()的。

好了,大概就是这样,记录于此,纠偏认知。



6、方法覆盖

在private和继承的关系中我们已经提到,当调用子类对象的方法或属性时,先看子类是否有,如果没有,就到父类对象(且权限允许的情况下)去调用。

这意味着,如果出现和父类同名的方法,调用时会执行子类的方法,而非父类的方法,这个叫方法覆盖,发生在继承关系中。当然,这要求同名、同参、同返回值,且访问权限不能缩小

另外在方法覆盖和异常抛出上,也有一定的限制:
  • 不可以增加新的异常,即使这个新的异常是父类方法声明中的任何一个异常的子类也不行
  • 不可以抛出 "被覆盖方法抛出异常" 的父类异常

就像一个修理家电的人,他能够修理冰箱,电脑,洗衣机,电视机。 一个年轻人从他这里学的技术,就只能修理这些家电,或者更少。你不能要求他教出来的徒弟用从他这里学的技术去修理直升飞机。

假如我有Cat子类继承Animal父类,父类有run方法,为何子类要覆盖run方法,而不是另外写catRun方法呢?很简单,如果不使用方法覆盖,那么在调用子类时实际其父类的方法也存在,即也可以调用,这不符合面向对象的封装性,这也是方法覆盖的意义所在。



7、Object类

Object类是所有类的父类,位于java.lang包中,任何类的对象,都可以调用Object类中的方法,包括数组对象。

7.1 toString

toString方法可以将任何一个对象转换为字符串返回,返回值的算法为:
getClass().getName() + '@' + Integer.toHexString(hashCode())

即“包名+类名+@+16进制数”,实际上System.out.print( Object obj )则默认调用了toString方法。另外,通常我们某些类的toString需要重写为我们需要的样式。

7.2 equals

equals其实本质上和“==”是一样的,都是比较的对象的虚地址,但是“==”是不能修改的,所以为了按照特定的方式进行两个对象的对比,比如我们希望两个对象的某个属性相同就视为相同的话,就可以采用重写equals方法的方式。

实际上Java中很多提供的类也重写了equals,比如String的equals就是比较两个字符串的内容,而非虚地址。

7.3 hashCode

hashCode方法是获取对象的哈希码,结果是16进制。

(哈希码:哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同)

我们在重写equals,往往要连同把hashCode方法进行重写,要求:
  • 如果两个对象用equals返回true,则它们的hashCode必须相同
  • 如果两个对象用equals返回false,那么它们的hashCode值不一定不同

为什么要重写hashCode?假如我们有Book类,两本同名书,但不同的出版社,现在我们希望只要名字相同就视为同一本书,我们重写equals,同名书返回了true,但是如果不重写hashCode,在涉及到Set集合时,比如存入Set集合中,会因为hashCode不同,而无法实现去重。


原文地址:https://www.cnblogs.com/deng-cc/p/7461887.html