一篇文章快速了解Java中的继承与多态

一. 继承

1.介绍

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。此处的多个类称为子类(此类,派生类,拓展类),单独的这个类称为父类(基类 ,超类)。可以理解为:“子类 is a 父类”。

类继承语法规则:
class Subclass extends SuperClass{ };

以下几点需要注意:

  • 子类继承了父类所有可以访问的数据域和方法
  • 父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私的结构。只因为封装性的影响,使得子类不能直接调用父类的结构而已。如果父类中定义了公共的访问器 / 修改器,那么可以通过这些公共的访问器 / 修改器来访问和修改它们。
    在这里插入图片描述
  • 子类并不是父类的一个子集。实际上,一个子类通常比它的父类包含更多的信息和方法
  • Java 中是不允许多重继承的。一个 Java 类只可能直接继承自一个父类。这种限制称为单一继承( single inheritance)。如果使用 extends 关键字来定义一个子类,它只允许有一个父类。然而,多重继承是可以通过接口来实现。
    在这里插入图片描述

2.super关键字

在Java类中使用super来调用父类中的指定操作:

  • 用于访问父类中定义的属性(属性没有私有化)
  • 用于调用父类中定义的成员方法
  • 用于在子类构造器中调用父类的构造器

2.1 调用父类的构造方法

构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用关键字 super 从子类的构造方法中调用。

调用父类构造方法的语法是:
super()或者super(parameters);

  • 语句 super() 调用父类的无参构造方法,而语句 super(arguments) 调用与参数匹配的父类的构造方法。语句 super() 和 super (arguments) 必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。
  • 子类中所有的构造器默认都会访问父类中空参数的构造器
  • 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错.所以呢,一般情况下,最好能为每个类提供一个无参构造方法,以便于对该类进行扩展,同时避免错误。
  • 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则**必须显式的使用"super.属性"**的方式,表明调用的是父类中声明的属性。
  • 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。
public class Person {
    private String name;
    private int age;
    private Date birthDate;
    public Person(String name, int age, Date d) {
        this.name = name;
        this.age = age;
        this.birthDate = d; }
    public Person(String name, int age) {
        this(name, age, null);
    }
    public Person(String name, Date d) {
        this(name, 30, d);
    }
    public Person(String name) {
        this(name, 30);
    }
}

public class Student extends Person {
    private String school;
    public Student(String name, int age, String s) {
        super(name, age);
        school = s; }
    public Student(String name, String s) {
        super(name);
        school = s; }
    // 编译出错: no super(),系统无法调用父类无参数的构造器。
    //There is no default constructor available in 'chapter01.Person'
    public Student(String s) {
        school = s; }
}

在这里插入图片描述

2.2 构造方法链

在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法当构造一个子类的对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止。这就是构造方法链(constructor chaining)。

public class Faculty extends Employee {
    public static void main(String[] args) {
        Faculty faculty = new Faculty();
    }

    public Faculty() {
        System.out.println("(4) Performs Faculty's tasks");
    }
}

class Employee extends Person {
    public Employee() {
        this("(2)Invoke Employee's overloaded constructor");
        System.out.println("(3)Performs Employee's tasks ");
    }

    public Employee(String s) {
        System.out.println(s);
    }
}

class Person {
    public Person() {
        System.out.println("(1) Performs Person's tasks");
    }
}

结果:
在这里插入图片描述
在第3 行,new Faculty() 调用Faculty 的无参构造方法。由于 Faculty 是 Employee 的子类,所以,在Faculty 构造方法中的所有语句执行之前,先调用 Employee 的无参构造方法。Employee 的无参构造方法调用Employee 的第二个构造方法(第13 行)。由于 Employee 是 Person 的子类,所以,在 Employee 的第二个构造方法中所有语句执行之前,先调用 Person 的无参构造方法。
在这里插入图片描述

2.3 调用父类的方法

super.方法名(参数);

3.方法重写

子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称作方法重
写(method overriding),:要重写一个方法,需要在子类中使用和父类一样的签名以及一样的返回值类型来对该方法进行定义。

注意以下几点:

  1. 仅当实例方法是可访问时,它才能被覆盖。因为私有方法在它的类本身以外是不能访问的,所以它不能被覆盖。如果子类中定义的方法在父类中是私有的,那么这两个方法完全没有关系。
  2. 子类不能用语法 super.super.toStringO 访问父类的父类中的toString ,这是一个语法错误。
  3. 与实例方法一样,静态方法也能被继承。但是,静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。可以使用语法:父类名 .静态方法名(SuperClassName.staticMethodName) 调用隐藏的静态方法。

这个再次详细说明一下

public class StaticExtends {

    public static void main(String[] args) {
        //声明为Father类,son1静态方法和Father类绑定
        Father son = new Son();
        
        son.method();
        son.staticMethod();

        Son son2 = new Son();
        son2.method();
        son2.staticMethod();
    }
}

class Father {

    void method() {
        System.out.println("父类方法");
    }

    static void staticMethod() {
        System.out.println("父类静态方法");
    }
}

class Son extends Father {

    @Override
    void method() {
        System.out.println("子类方法");
    }

    static void staticMethod() {
        System.out.println("子类静态方法");
    }
}

输出结果:
在这里插入图片描述
在子类中重写父类的static方法,是不会报错的,编译也可以通过,但是在通过一个声明为父类,实际类型为子类的引用变量调用该方法时,发现被调用的仍是父类中原本以为会被覆盖的方法,不具有“多态”特性。所以呢,父类的static方法是不会被重写的。

4.Object类及其常用方法

Java 中的所有类都继承自 java.lang.Object 类,如果在定义一个类时没有指定继承性,那么这个类的父类就被默认为是 Object。
在这里插入图片描述

4.1 toString()方法

Object 类中toString()方法的默认实现是:

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

调用一个对象的 toString() 会返回一个描述该对象的字符串。默认情况下,它*返回一个由该对象所属的类名、at 符号(@)以及该对象十六进制形式的内存地址组成的字符串。*这个信息不是很有用,所以重写。

  • 像String、Date、File、包装类等都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回"实体内容"信息。
  • 可以根据需要在用户自定义类型中重写toString()方法。
    如String 类重写了toString()方法,返回字符串的值。
s1=“hello”;
System.out.println(s1);//相当于System.out.println(s1.toString());
  • 基本类型数据转换为String类型时,调用了对应包装类的toString()方法
int a=10; 
System.out.println(“a=+a);

4.2 equals()方法

Object 类中 equals 方法的默认实现是:

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

说明:Object类中定义的equals()和==的作用是相同的:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体。

  • 像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。
  • 通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的"实体内容"是否相同。那么,我们 就需要对Object类中的equals()进行重写。重写的原则:比较两个对象的实体内容是否相同

tips:==和equals的区别:

  • 1 == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
  • 2 equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
  • 3 具体要看自定义类里有没有重写Object的equals方法来判断。
  • 4 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

5. 防止拓展与重写 —final关键字

在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”。

  1. final标记的类不能被继承。 提高安全性,提高程序的可读性。
- String类、System类、StringBuffer类 
  1. final标记的方法不能被子类重写。
    • 比如:Object类中的getClass()。
  2. final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。
    • final double MY_PI = 3.14;
    • final标记的成员变量必须在声明时或在每个构造器中或代码块中显式赋值,然后才能使用。
public final class Test {
public static int totalNumber = 5;
public final int ID;
public Test() {
ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
final int I = 10;
final int J; J = 20;
J = 30; // 非法
}
 }
  
  

二. 多态

6.1 介绍

首先呢,我们知道继承关系使一个子类继承父类的特征,并且附加一些新特征。子类是它的父类的特殊化,每个子类的实例都是其父类的实例,但是反过来就不成立。例如:每个圆都是一个几何对象,但并非每个几何对象都是圆。因此,总可以将子类的实例传给需要父类型的参数。使用父类对象的地方都可以使用子类的对象。这就是通常所说的多态。简单来说,多态意味着父类型的变量可以引用子类型的对象。

6.2 动态绑定

我们都知道方法可以在父类中定义而在子类中重写。(方法可以在沿着继承链的多个类中实现。JVM 决定运行时调用哪个方法。)那么

Object o = new SonObject();
System.out.println(o.toSting);

这里的 o 调用哪个 tostring() 呢?
我们首先介绍两个术语:声明类型和实际类型。**一个变量必须被声明为某种类型。变量的这个类型称为它的声明类型(declared type)。**这里,o 的声明类型是 Object。一个引用类型变量可以是一个 null 值或者是一个对声明类型实例的引用。实例可以使用声明类型或它的子类型的构造方法创建。变量的实际类型(actual type) 是被变量引用的对象的实际类。这里,o 的实际类型是SonObject, 因为 o 指向使用 new SonObject() 创建的对象。o 调用哪个toString() 方法由 o 的实际类型决定。这称为动态绑定(dynamic binding)。
也就是多态情况下,编译时,看左边;运行时,看右边。
“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)

态绑定工作机制如下:假设对象 o 是类 Cl, C2, … ,Cn-1, Cn 的实例,其中 C1是 C2的子类,C2 是 C3 的子类,… ,Cn-1是 Cn 的子类。也就是说,Cn 是最通用的类,C1是最特殊的类。在 Java 中,Cn 是 Object 类。如果对象 o 调用一个方法 p, 那么JVM 会依次在类 Cl,C2, … ,Cn-1,Cri 中查找方法 p 的实现,直到找到为止一旦找到一个实现,就停止査找,然后调用这个首先找到的实现。
在这里插入图片描述
看以下代码:

public class DynamicBindDemo {
    public static void main(String[] args) {
        m(new GraduateStudent());
        m(new Student());
        m(new Person());
        m(new Object());
    }

    public static void m(Object x) {
        System.out.println(x.toString());
    }
}

class GraduateStudent extends Student {

}

class Student extends Person {
    @Override
    public String toString() {
        return "Student";
    }
}

class Person {
    @Override
    public String toString() {
        return "Person";
    }
}

输出结果:
在这里插入图片描述

6.3 对象转换和instanceof()运算符

6.3.1 对象转换

对象的引用可以类型转换为对另外一种对象的引用,这称为对象转换。

Object o = new Student();
m(o);

Student 的实例也是 Object 的实例,所以,语句 Object o = new StudentO 是合法的,它称为隐式转换(implicit casting)。
但是Student b = o;将会发生编译错误,原因是 Student 对象总是 Object 的实例,但是,Object 对象不一定是 Student 的实例。即使可以看到 0实际上是一个 Student 的对象,但是编译器还没有聪明到知道这一点。为了告诉编译器o就是一个 Student 对象,就要使用显式转换( explicit casting)。

Student b = (Student)o;// Explicit casting

总是可以将一个子类的实例转换为一个父类的变量,称为向上转换(upcasting),因为子
类的实例永远是它的父类的实例。当把一个父类的实例转换为它的子类变量(称为向下转换
(downcasting)) 时,必须使用转换记号 “(子类名)” 进行显式转换,向编译器表明你的意图。
为使转换成功,必须确保要转换的对象是子类的一个实例。如果父类对象不是子类的一个
实例,就会出现一个运行异常 ClassCastException。

6.3.2instanceof运算符

在尝试转换之前确保该对象是另一个对象的实例,可以利用运算符instanceof 来实现的。

Objecto = new Circle();
if (o instanceof Circle){
Circle c = ((Circle)o;
System.out.println("The circle diameter is + o.getDiameter());
  • ① a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false。
  • ② 如果 a instanceof A返回true,则 a instanceof B也返回true.其中,类B是类A的父类。
  • ③ 要求a所属的类与类A必须是子类和父类的关系,否则编译错误。

变量 myObject 被声明为 Object。**声明类型
决定了在编译时匹配哪个方法。**使用 myObject.getDiameter()会引起一个编译错误,因为Object 类没有 getDiameter 方法。编译器无法找到和 myObject.getDiameter()匹配的方法。所以,有必要将 myObject 转换成 Circle 类型,来告诉编译器 myObject 也是 Circle 的一个实例。同时要注意,引用变量 o和 c指向同一个对象,而在进行基本数据类型转换时,会创建一个新的对象。

为什么没有在一开始就把 myObject定义为 Circle 类型呢?为了能够进行通用程序设计,一个好的经验是把变童定义为父类型,这样,它就可以接收任何子类型的值。
在这里插入图片描述

注意::对象成员访问运算符( .)优先于类型转换运算符。使用圆括号保证在点运算符( .)之前进行转换,例如:((Circle)object).getArea();

发布了6 篇原创文章 · 获赞 17 · 访问量 349

猜你喜欢

转载自blog.csdn.net/weixin_46215617/article/details/105443361