Java面向对象之继承(Inheritance)

1,439 阅读12分钟

v2-a8fbc8147fcdeabc8dee9e252405b52f_r.jpg

继承其实很好理解,我们天生就会继承来自父母的很多基因,那父亲拥有的很多能力你天生就会拥有甚至会发展的比父亲好,就如博人丸子比鸣人搓的好,向日葵白眼比雏田强,他们的能力都是继承于【父类】,并在此基础上进行了发展,这种发展我们就称为【重写】,那么就让我们一起来看看继承在代码中的用法👇

🔥继承概念

一个类可以继承一个类,被继承的类我们称之为【父类】或者【超类】,另一个类称之为【子类】也叫【派生类】,子类可以通过extends关键字轻松拥有获取父类的成员变量和成员方法的能力,除了被private修饰的。在java中是单继承的,这样可以规范代码的实现。继而

1> 继承的好处

<1>提高了代码的复用性,代码常见的三种复用方式:继承、组合、代理;

<2>类与类之间产生了关系,这是构成多态的前提;

2> 代码测试

接着我们通过代码尝试理解下继承:

//定义父类-波风水门,创建姓名属性和攻击的方法
public class ShuiMen {
    private String name = "波风水门";
    
    public void attack(){
        System.out.println("螺旋丸");
    }
}
//定义子类-漩涡鸣人,继承父类-波风水门,创建姓名属性
public class MingRen extends ShuiMen{
    private String name = "漩涡鸣人";
}
//训练场测试
public class Test {
    //创建鸣人的对象,看是否可以继承水门的螺旋丸
    public static void main(String[] args) {
        MingRen mingRen = new MingRen();
        mingRen.attack();
    }
}

让我们在训练场进行测试:

执行1.png

🌌方法重写

波风水门创造出了螺旋丸,我们知道漩涡鸣人在螺旋丸的基础上就将螺旋丸改进出了近20种不同的螺旋丸,如大玉螺旋丸太极螺旋丸惑星螺旋丸仙法·磁遁·螺旋丸……,那结合上述的代码,我们是不是可以让子类MingRen改进父类ShuiMen的attack方法,答案是可以的,而这种改进就是方法的重写。

1> 方法重写概念

子类中出现与父类一模一样的方法时(返回值类型、方法名、参数列表相同),子类中的方法会覆盖父类中的方法,这种覆盖就是重写。

重写的方法上面加上注解@Override,表示该方法进行了重写。

2> 重写的应用

我们在工作中可以根据自己的需求,对我们所继承的父类中的一些方法进行重写,在子类中定义一些特定的属性行为,从而对弗雷德方法进行扩展增强。

注意:重写父类的方法时,子类不能缩小父类方法的范围和权限。

3> 代码测试

接替上述代码,我们实现下覆盖既重写,在MingRen类中改动:

//定义子类-漩涡鸣人,继承父类-波风水门,创建姓名属性,重写父类攻击方法
public class MingRen extends ShuiMen{
    private String name = "漩涡鸣人";

    @Override
    public void attack() {
        System.out.println("哈~超大玉螺旋丸");
    }
}

让我们在训练场进行测试:

执行2.png

💖super关键字

是不是浮现出了this关键字呢(☞゚ヮ゚)☞,我们知道this指向的是调用该方法的实例对象;同样的super关键字指向的是父类的实例对象;

1> 代码实例

现在我们先给父类和子类添加无参构造方法,然后我们在训练场实例化子类;

//定义父类-波风水门,创建姓名属性,无参构造
public class ShuiMen {
    private String name = "波风水门";

    public ShuiMen() {
        System.out.println("我是波风水门");
    }
}
//定义子类-漩涡鸣人,继承父类-波风水门,创建姓名属性,无参构造
public class MingRen extends ShuiMen{
    private String name = "漩涡鸣人";

    public MingRen() {
        System.out.println("我是旋涡鸣人");
    }
}
//训练场测试
public class Test {
    //创建鸣人的对象,看是否可以执行水门的构造方法
    public static void main(String[] args) {
        MingRen mingRen = new MingRen();
    }
}

训练场测试结果如下:

执行3.png

2> 继承中构造方法的相关问题

结论:通过上述执行结果我们可以看出,构造一个子类一定会先构造一个父类,如果构造孙子,也是先会构造祖父类的,不妨我们可以加入BoRen类,然后再训练场构造BoRen实例:

//定义子类-漩涡博人,继承父类-旋涡鸣人,创建姓名属性,重写父类攻击方法
public class BoRen extends MingRen{
    private String name = "漩涡博人";

    public BoRen() {
        System.out.println("我是漩涡博人");
    }
}
//训练场测试
public class Test {
    //创建博人的对象,看是否可以执行水门的构造方法
    public static void main(String[] args) {
        BoRen boren = new BoRen();
    }
}

训练场测试结果如下:

执行4.png

因此我们可以总结出两点:

1> 使用super调用父类的非私有方法和属性时,大致过程如下:

  1. 先在当前类中寻找。
  2. 当前类没有,继续向父类中寻找。
  3. 如果还是没有,就向父类的父类继续寻找。
  4. 直到到达一个所有类的共同父类,他叫Object

那么问题来了,我想使用父类的属性,直接用就行了,super有啥用啊。是啊,那如果子类也定义了相同名字的属性呢?因此我们还是需要super里访问父类的属性。

2> 在子类的构造方法中,访问父类的构造方法

我们结合上述的代码,再深入探索一下子类调用的是父类的无参构造还是有参构造,我们在ShuiMen类中增加无参构造和有参构造及get,set方法,在MingRen类中也添加,然后我们进行测试:

//定义父类-波风水门
public class ShuiMen {
    private String name = "波风水门";

    public ShuiMen() {
        System.out.println("我是波风水门");
    }

    public ShuiMen(String name) {
        this.name = "四代目";
    }

    public String getName() {
        return name;
    }
}
//定义子类-漩涡鸣人,继承父类-波风水门
public class MingRen extends ShuiMen{
    private String name = "漩涡鸣人";

    public MingRen() {
        System.out.println("我是旋涡鸣人");
    }

    public MingRen(String name) {
        this.name = name;
    }
}
//训练场测试
public class Test {
    //创建鸣人的对象,打印名字属性,看是否执行了有参构造
    public static void main(String[] args) {
        MingRen mingRen = new MingRen();
        System.out.println(mingRen.getName());
    }
}

执行结果如下:

执行5.png

由执行结果我们可以看出,创建MingRen类实例时默认执行了两个无参构造,并且在ShuiMen类中有参构造没有被执行,切打印出来的名字依旧是波风水门,因此子类在构造的时候只会默认调用父类的【空参构造】。那如果子类要通过父类的有参构造,又该怎么办呢?我们对上述代码进行改进:

//定义父类-波风水门
public class ShuiMen {
    private String name = "波风水门";

    public ShuiMen() {
        System.out.println("我是波风水门");
    }

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

    public String getName() {
        return name;
    }
}
//定义子类-漩涡鸣人,继承父类-波风水门
public class MingRen extends ShuiMen{
    private String name = "漩涡鸣人";

    public MingRen() {
        super("四代目");
        System.out.println("我是旋涡鸣人");
    }

    public MingRen(String name) {
        super(name);
    }
}
//训练场测试
public class Test {
    //创建鸣人的对象,打印名字属性,看是否执行了有参构造
    public static void main(String[] args) {
        MingRen mingRen = new MingRen();
        System.out.println(mingRen.getName());
    }
}

执行结果如下:

执行6.png

由此我们可以看出此程序先执行了子类的空参构造,然后通过Super()调用了父类的有参构造,从而实现了子类访问父类有参构造的需求;在此程序中,我们也可以总结出两个知识点:

1> super()this()只能放在方法中的第一行,并且在一个方法中不能同时出现

在改进上述代码时,我们发现了如下图所示错误:

执行7.png

其中指明Call to 'super()' must be first statement in constructor body,因此【super构造器只能放在第一行】,如图所示:

执行8.png

其实很好理解,上述讲到的那个结论提到【构造一个子类一定会先构造一个父类,如果构造孙子,也是先会构造祖父类的】,所以父类还没有构造,你的代码凭什么执行?

2> super()this()不会向上检索

this

super

访问属性

访问本实例的属性,没有会继续向父类检索

访问父类实例的属性,没有会继续向父类检索

调用方法

访问本实例的方法,没有会继续向父类检索

访问父类实例的方法,没有会继续向父类检索

调用构造器

调用本类的构造器,必须放在第一行,不会向上检索

调用父类的构造器,必须放在第一行,不会向上检索

3> 总结

对super关键字总结以下几点:

  1. 子类继承了父类所有的非私有的属性和方法,可以直接调用。
  2. 子类在构造的时候,一定会构造一个父类,默认调用父类的无参构造器。
  3. 子类如果希望指定去调用父类的某个构造器, 则显式的调用一下 : super(参数列表)
  4. super和this当做构造器使用时, 必须放在构造器第一行,所以只能二选一。
  5. java 所有类都是 Object 类的子类, Object 是所有类的基类.
  6. 子类最多只能继承一个父类(指直接继承), java 中是单继承机制,我们可以使用连续继承来实现。

🍜final关键字

final作为一个关键字,他可以修饰变量,方法,以及类,final就是最终的意思:

1、被final修饰的变量不能被修改,这里有两层含义,如果final修饰的是基础数据类型是只不能被修改,如果是引用数据类型就是引用指向不能被修改。

执行9.png

2、被final修饰的方法不能被重写

执行1.png

3、被final修饰的类不能被继承

执行2.png

🐱‍👤祖先类Object

Object类有11个方法,其中有八个是公共方法,在此我们先学习几个基本的方法:

(1) hashCode

方法的定义:

public native int hashcode();

所有带有native的方法都是本地方法,他不是java写的。这个hashcode的返回值其实是实例对象运行时的内存地址。hash算法是其发展的重点,那什么是hash算法呢?

一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。

hash算法的几个特点:

  1. 只能通过原文计算出hash值,而且每次计算都一样,不能通过hash值计算原文;
  2. 原文的微小变化就能使hash值发生巨大变化;
  3. 一个好的hash算法还能尽量避免发生hash值重复的情况,也叫hash碰撞。

hash算法的用途:

  1. 密码的保存;
  2. 文件的校验,检查数据的一致。

常见的hash算法:

MD5

import java.security.MessageDigest;
import java.util.Arrays;

public class Hash {
    public static void main(String[] args) throws Exception {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] digest = md5.digest("123".getBytes());
        System.out.println(Arrays.toString(digest));
    }
}

执行3.png

SHA1

public class Hash {
    public static void main(String[] args) throws Exception {
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        byte[] digest = sha1.digest("123".getBytes());
        System.out.println(Arrays.toString(digest));;
    }
}

执行4.png

SHA256:SHA256算法使用的哈希值长度是256位。

public class Hash {
    public static void main(String[] args) throws Exception {
        MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
        byte[] digest = sha1.digest("123".getBytes());
        System.out.println(Arrays.toString(digest));;
    }
}

执行5.png

SHA512:SHA512算法使用的哈希值长度是512位。

public class Hash {
    public static void main(String[] args) throws Exception {
        MessageDigest sha1 = MessageDigest.getInstance("SHA-512");
        byte[] digest = sha1.digest("123".getBytes());
        System.out.println(Arrays.toString(digest));;
    }
}

执行6.png

(2) equals

Tips:较所有的引用数据类型使,都要使用equals

Object中equals方法的源码如下:

public boolean equals(Object obj) {
    return (this == obj);
}

【总结】

  1. ==可以比基础数据类型也可以比较引用数据类型,比较基础数据类型时比较的是具体的值,比较引用数据类型实际上比较的是内存地址
  2. equals是Object的一个方法,默认的实现就是 ==。
  3. 我们可以重写equals方法,满足我们的特性需求,比如String就重写了equals方法,所以字符串调用equals比较的是每一个字符。

比如,编写一个类Student,我们需要比较两个学生的信息,如果编号和姓名一样,我们就认为是同一个人,因此我们就可以重写equals方法,具体如下:

public class Student {
    //创建编号、名字、性别属性
    private int id;
    private String name;
    private String sex;

    //创建构造方法

    public Student(int id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }

    //创建get\set方法

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getSex() {
        return sex;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    //重写equals()
    @Override
    public boolean equals(Object o){
        if (this == o) return true;
        if (o == null) return false;
        Student student = (Student) o;
        return id == student.id && student.getName().equals(name);
    }

    //主函数进行测试
    public static void main(String[] args) {
        Student student1 = new Student(1,"张三","男");
        Student student2 = new Student(1,"张三","女");
        System.out.println(student1.equals(student2));
    }
}

执行结果如下:

执行1.png

(3) toString

此方法就是将一个实例化对象转化成一个可以打印的字符串。默认的打印的方法就是默认调用Student的toString方法。

在上面代码的基础上我们对toString()进行测试训练:

//重写toString()
@Override
public String toString() {
    return "Student{" +
            "id=" + id +
            ", name='" + name + ''' +
            ", sex='" + sex + ''' +
            '}';
}
//主函数进行测试
public static void main(String[] args) {
    Student student1 = new Student(1,"张三","男");
    Student student2 = new Student(1,"张三","女");
    //此处可以不用student1.toString(),因为打印方法会默认调用刚才重写的toString()
    System.out.println(student1);
}

结果如下:

执行1.png

(4) clone

克隆就是在内存里边赋值一个实例对象。但是Object的克隆方法只能浅拷贝。同时必须实现Cloneable接口。

深拷贝与浅拷贝的区别: 浅拷贝就如鸣人的影分身,都使用的是鸣人自己的查克拉;而深拷贝就像卡卡西的复制,鸣人用自己的查克拉进行影分身,卡卡西copy后也可以用自己的查克拉进行影分身。

在Java中深拷贝和浅拷贝指的都是对象的拷贝:

浅拷贝: 被复制的对象的所有的变量都与原对象有相同的值,而所有的引用对象仍然指向原来的对象。

深拷贝: 除了被复制对象的所有变量都有原来对象的值之外,还把引用对象也指向了被复制的新对象。