Java 基础学习(八)多态、接口、造型与内部类

1 多态

1.1 多态

1.1.1 多态的意义

一个类型的引用在指向不同的对象时会有不同的实现。依然借助前面案例中的 Person类、Student类和 Teacher 类举例,看如下的代码:

复制代码
Person p1 = new Student();
Person p2 = new Teacher();
p1.schedule();
p2.schedule();

同样声明为 Person 类型的变量 p1和p2,当指向不同的对象时,可以有不同的表现。这种现象在 Java中被称为多态。

多态是面向对象的三大基本特征之一,包含:

  • 个体多态
  • 行为多态

1.1.2 什么是多态

多态的定义:计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。

这个定义很晦涩,可以从两个方面理解:

1、个体多态

  • 父类型定义的变量引用的子类型个体是多种多样的,如:Person变量可以引用Student、Teacher、Worker对象等
  • 个体多态是用向上造型实现的

2、行为多态

  • 父类型变量引用子类型实例后,执行方法时候可以得到多种结果,如:Person变量分别引用Student、Teacher、Worker对象时候执行schedule方法得到的结果不同
  • 行为多态是利用方法重写实现的

1.1.3 为什么需要多态

比如,有类关系如下图所示:

如何使用多态呢?首先多态的前提是继承,在有继承关系存在时候才能使用多态,其次是要对子类型进行统一处理,比如,所以人员都要执行计划。具体在实际开发中可以先不要考虑多态,先按照具体类型编程,当发现各种子类型都需要相同处理时候,再重构代码提升到父类型统一进行处理,这样就是多态处理了。比如:学生要执行计划schedule,教师要执行计划schedule,工人也要执行计划schedule,就统一装到Person数组中,用循环调用schedule方法一起执行计划。

1.1.4【案例】多态的基础应用示例

在上一个案例的基础上,测试行为多态。

案例示意代码如下:

复制代码
public class PolyDemo1 {
    public static void main(String[] args) {
        // 使用父类引用指向子类对象 -> 子类向上造型
        Person p1 = new Student("Tom", 12);
        Person p2 = new Teacher("Andy", 28);
        Person p3 = new Worker("Jerry", 28);
        // 父类引用调用抽象方法,实际执行子类实现的方法
        // -> 行为多态
        p1.schedule();
        p2.schedule();
        p3.schedule();
        // 父类引用仅能访问父类中声明的成员
        // 父类应用无法访问子类中声明的成员
        // p1.study(); // 编译错误,无法访问
    }
}

1.1.5【案例】多态的扩展应用示例

在上一个案例的基础上,使用数组存储多个对象,统一执行其 schedule() 方法。

案例示意代码如下:

复制代码
package oop_04.polymorphic;
public class PolyDemo2 {
    public static void main(String[] args) {
        // 多态数组
        Person[] array = {new Student("Tom", 12)
                            ,new Teacher("Andy", 28)
                            ,new Worker("Jerry", 28)};
        for(int i=0;i<array.length;i++) {
            test(array[i]);
        }
        Student s1=new Student("Lucy",15);
        // 子类类型的对象也可以传入该方法
        test(s1);
    }
    /**
     * 方法的参数为父类形态
     * 该父类的任意子类对象均可以传入该方法
     * @param person
     */
    public static void test(Person person){
        person.schedule();
    }
}

2 接口 interface

2.1 什么是接口

2.1.1 多继承问题

讨论接口之前,先看一个物品归类的生活实例:

查看这个商品分类,可看出:

1、相同类别的商品具有相似特征,且属性类似:比如"新鲜水果"类、"海鲜水产"类

2、同类标签便于管理统一存储和调度

3、也存在跨类别的分类标签:比如"地方特产"、"国际美食"

4、跨类别的分类标签体现了一个物品属于多种类型的现象,这种现象称为"多继承"

如果用 Java 代码来表现上述情况,会发现,Java的继承可以实现树形分类,但是无法处理跨类别标签:

不过,Java提供了接口,解决了跨类型的继承问题。

2.1.2 什么是接口

接口在JAVA编程语言中是一个抽象类型,通常以interface来声明。

从面向对象编程的角度,可以将接口理解为对不同类型的事物的共同的行为特征的抽象。例如,鹰和飞机属于不同类型的事物,但是都有飞行的行为特征。

可以把接口看成是特殊的抽象类。

2.1.3 接口和抽象类

抽象类和接口都属于抽象的概念,它们有一些区别,可以从同类别和跨类别的角度来考虑:

  • 同一种类别的公共行为和属性可以抽取到抽象类中。抽象类用于表示一种具有共性的类,可以包含实现的方法和具体的属性。比如,对于喜鹊和老鹰这两种鸟类,它们都属于鸟类的范畴,可以将它们共同的行为和属性抽象到一个抽象类(如Bird)中,以实现代码的重用和扩展。
  • 不同种类的公共行为可以抽取到接口中。接口用于定义一组相关的方法,用于表示某种能力或行为。比如,喜鹊、老鹰和飞机都具有起飞和着陆的功能,但它们并不属于同一种类,此时可以将与飞行相关的共同行为抽取到一个接口(如Flyable)中,不同类别的对象可以通过实现该接口来具备飞行的能力。

根据以上原则,对于喜鹊来说,它可以继承自抽象类Bird,以获取鸟类的共性属性和行为,并且还可以实现接口Flyable,以具备飞行的能力。

抽象类和接口的设计原则:

  • 将所有子类共有的方法抽象化到父类中,可以使用抽象类。
  • 将部分子类中的公共方法抽象化到接口中,适用于不同类别但具有相似行为的对象。

通过合理地使用抽象类和接口,可以实现代码的复用和扩展,并且更好地表示对象之间的关系和行为。选择使用抽象类还是接口取决于具体的设计需求和对象之间的关系。

2.2 接口的语法

2.2.1 接口的语法

使用interface定义接口:

1、接口中只能定义常量和方法

  • 可以省略常量的修饰词 public static final
  • 可以省略抽象方法修饰词 public abstract

2、接口不能实例化创建对象,

3、接口只能被继承,作为父类型被子类型实现

比如,定义飞行接口:

  • 包含常量ID
  • 包含 3 个抽象方法

代码结构示意如下:

2.2.2 实现接口

子类使用implements实现接口:必须实现该接口中所有的抽象方法。

具体语法如下所示:

一个类可以实现多个接口:实现的接口直接用逗号分隔。

具体语法如下所示:

2.2.3【案例】接口的示例

定义接口 Flyable 和类 Bird,并类 Plane实现接口Flyable,以及类 Eagle 继承Bird并实现Flyable;编写代码测试接口的用法。

案例示意代码如下所示:

复制代码
package oop_04.interface01;
/**
 * 飞行接口
 */
public interface Flyable {
    int ID = 1;
    /**
     * 起飞
     */
    void takeOff();
    /**
     * 飞行
     */
    void fly();
    /**
     * 着陆
     */
    void land();
}
package oop_04.interface01;
public class Plane implements Flyable{
    @Override
    public void takeOff() {
        System.out.println("Plane takeOff...");
    }
    @Override
    public void fly() {
        System.out.println("Plane fly...");
    }
    @Override
    public void land() {
        System.out.println("Plane land...");
    }
}
package oop_04.interface01;
public class Bird {
    public void eat(){
        System.out.println("eat...");
    }
    public void sleep(){
        System.out.println("sleep...");
    }
}
package oop_04.interface01;
public class Eagle
        extends Bird implements Flyable{
    @Override
    public void takeOff() {
        System.out.println("Eagle takeOff...");
    }
    @Override
    public void fly() {
        System.out.println("Eagle fly...");
    }
    @Override
    public void land() {
        System.out.println("Eagle land...");
    }
}
package oop_04.interface01;
public class InterfaceDemo1 {
    public static void main(String[] args) {
        // Flyable flyable=new Flyable();  // 接口不可被实例化
        System.out.println(Flyable.ID);    // 接口中定义的是静态常量
        Flyable eagle = new Eagle();       // 接口类型引用指向实现类的对象
        eagle.fly();                       // 实际执行实现类重写的方法逻辑
        Flyable plane = new Plane();       // 接口类型引用指向实现类的对象
        plane.fly();                       // 实际执行实现类重写的方法逻辑
        // eagle.eat();                    // 无法访问实现类特有的方法
    }
}

2.3 接口与多继承

2.3.1 接口的继承

在Java中,接口之间也可以进行继承,这被称为接口的继承或接口的扩展。

接口的继承允许一个接口继承另一个接口的方法签名。通过继承,子接口可以获得父接口定义的方法签名,并且可以在子接口中添加新的方法签名。子接口继承了父接口的方法签名后,必须提供这些方法的具体实现。

接口的继承使用关键字extends,后面跟着要继承的父接口名称。一个接口可以继承多个接口,多个父接口之间使用逗号分隔。

下面是一个接口继承的示例:

复制代码
interface Shape {
    void draw();
}
interface Circle extends Shape {
    double getRadius();
}
interface Colorable {
    void setColor(String color);
}
interface ColoredCircle extends Circle, Colorable {
    void rotate();
}

在上面的示例中,接口Circle继承了接口Shape,表示Circle接口扩展了Shape接口的方法签名。接口ColoredCircle继承了接口Circle和Colorable,表示ColoredCircle接口扩展了这两个父接口的方法签名,并且可以在子接口中添加新的方法签名rotate()。

接口的继承使得接口之间可以建立层次结构,从而实现方法签名的复用和组合。通过继承,我们可以定义更具体和特定的接口,以满足不同的需求和功能。

2.3.2 接口与多继承

接口与多继承的关系是一个常见的面试话题。在Java中,类只能继承自一个父类,这是单继承的限制。然而,一个类可以实现多个接口,这就允许了多继承的实现。

多继承的概念意味着一个类可以从多个父类继承属性和方法。然而,在Java中,类只能继承一个父类。这就是为什么Java引入接口的原因,以实现多继承的效果。通过实现多个接口,一个类可以获得多个接口定义的行为和功能,实现了类的多继承。

如下图所示:

在这个示例中,有三个接口:Person(人)、Flyable(可飞行)和Swimmable(可潜水)。

钢铁侠类实现了Person接口,并且还实现了Flyable和Swimmable接口。这意味着钢铁侠类具有Person接口定义的人的行为和属性,同时也具有Flyable接口定义的飞行行为和Swimmable接口定义的潜水行为。

通过实现多个接口,钢铁侠类获得了多个接口定义的行为和功能,实现了类的多继承效果。钢铁侠类可以同时表现出人的特征、飞行的能力和潜水的能力。

接口的多继承使得类可以在不受单继承限制的情况下,获得多个接口定义的功能,提供了更大的灵活性和可扩展性。

2.3.3 经典面试题目:接口和抽象类的区别

面试时候可以尝试从语法层面回复这个问题:

接口和抽象类在语法上有一些区别,主要涉及以下几个方面:

声明方式:抽象类使用 abstract 关键字进行声明,使用 class 关键字定义类。接口使用 interface 关键字进行声明。

继承关系:抽象类通过使用 extends 关键字继承其他类或抽象类。一个类只能继承一个抽象类。接口通过使用 implements 关键字实现一个或多个接口。一个类可以实现多个接口。

方法实现:抽象类可以包含实现的方法和抽象的方法。接口只能包含抽象的方法,不包含具体的方法实现。所有的方法都隐式地被声明为抽象方法,不需要使用 abstract 关键字。实现接口的类必须提供方法的具体实现。Java 8 引入了接口中的静态方法和默认方法,使得接口具备了一定的实现能力。

2.3.4 经典面试题目:Java如何实现多继承的

在Java中,类是单继承的,即一个类只能继承自一个父类。然而,通过接口的使用,Java可以实现多继承的效果。类可以实现多个接口中的方法,从而获得多个接口定义的行为和功能。这种机制提供了灵活性和可扩展性,使得Java在面对多继承需求时能够更好地满足设计和开发的需要。

3 造型

3.1 向上造型

3.1.1 向上造型

向上造型(Upcasting)是指将一个子类对象赋值给父类引用变量的过程。通过向上造型,可以将一个子类对象视为其父类类型,实现多态性的体现。

向上造型的特点:

  • 子类对象可以赋值给父类引用变量,但是父类对象不能赋值给子类引用变量。
  • 向上造型是自动进行的,不需要额外的转换操作。
  • 通过向上造型,可以调用父类中声明的方法,但无法调用子类中特有的方法。

向上造型的优势:

  • 实现多态性:通过向上造型,可以将不同子类的对象视为父类类型,统一对待,实现多态性的效果。
  • 灵活性和扩展性:通过向上造型,可以在不改变父类引用的情况下,使用不同的子类对象,使程序具备更大的灵活性和可扩展性。

3.1.2 【案例】向上造型示例

示例代码:

复制代码
class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void printInfo() {
        System.out.println("Person: " + name);
    }
}
class Student extends Person {
    private int studentId;
    public Student(String name, int studentId) {
        super(name);
        this.studentId = studentId;
    }
    public int getStudentId() {
        return studentId;
    }
    public void printInfo() {
        System.out.println("Student: " + getName() + ", Student ID: " + studentId);
    }
}
class Teacher extends Person {
    private String subject;
    public Teacher(String name, String subject) {
        super(name);
        this.subject = subject;
    }
    public String getSubject() {
        return subject;
    }
    public void printInfo() {
        System.out.println("Teacher: " + getName() + ", Subject: " + subject);
    }
}
public class Main {
    public static void main(String[] args) {
        Person person1 = new Student("Alice", 123);
        Person person2 = new Teacher("Bob", "Math");
        person1.printInfo(); // 输出: Student: Alice, Student ID: 123
        person2.printInfo(); // 输出: Teacher: Bob, Subject: Math
        // 向上造型
        Person person3 = new Student("Carol", 456);
        Person person4 = new Teacher("David", "Science");
        person3.printInfo(); // 输出: Student: Carol, Student ID: 456
        person4.printInfo(); // 输出: Teacher: David, Subject: Science
    }
}

在上述示例中,Person是一个基类,Student和Teacher是其子类。我们可以将Student和Teacher对象向上造型为Person类型,并将它们赋值给Person引用变量person3和person4。通过向上造型,我们可以使用Person引用变量调用Person类中的方法和属性。在调用printInfo()方法时,由于方法被子类重写,实际上会根据对象的实际类型调用相应的子类方法。

这样做的好处是,我们可以使用统一的Person类型处理不同类型的对象,实现了多态性。通过向上造型,我们可以灵活地处理不同类型的子类对象,使代码更具扩展性和可维护性。

3.2 向下造型

3.2.1 向下造型

向上造型中有个特性"无法调用子类中特有的方法",如果调用子类行特有的方法呢?这个就需要使用向下造型了。

向下造型(Downcasting)是指将一个已经向上造型(Upcasting)的对象重新转回其原始的子类类型。它允许我们在需要的时候访问和调用子类特有的方法和属性。

3.2.2【案例】向下造型示例

在 Java 中,向下造型需要使用强制类型转换操作符(子类类型)来实现,但在进行向下造型之前,需要先确保对象实际上是指定的子类对象。否则,如果尝试对一个不兼容的对象进行向下造型,将会抛出ClassCastException异常。

以下是一个示例,演示了向下造型的使用:

复制代码
/**
 * 演示向下转型
 */
public class Demo02 {
    public static void main(String[] args) {
        Person person1 = new Student("Alice", 123);
        Person person2 = new Teacher("Bob", "Math");
        // 直接使用类型引用调用子类特有的方法会编译错误
        // person1.getStudentId(); // 编译错误
        // person2.getSubject(); // 编译错误
        
        // 向下转型
        Student student = (Student) person1;
        Teacher teacher = (Teacher) person2;
        System.out.println(student.getStudentId()); // 输出: 123
        System.out.println(teacher.getSubject()); // 输出: Math
        
        // 向下转型时,如果类型不匹配,会抛出ClassCastException
        // Teacher teacher2 = (Teacher) person1; // 抛出ClassCastException
    }
}

需要注意的是,在进行向下造型之前,我们需要确保对象实际上是指定的子类对象。否则,如果尝试对一个不兼容的对象进行向下转型,将会导致运行时异常。

向下转型的使用需要谨慎,应确保转型操作的合法性和正确性。如果不确定对象是否适合进行向下转型,可以使用instanceof运算符进行类型检查,以避免可能的异常。

3.2.3 instanceof 运算

instanceof 运算符用于检测对象是否是指定类型的实例。它经常与向下造型一起使用,以实现类型安全的转换,避免类型转换异常的发生。

instanceof 运算符的语法如下:

复制代码
对象 instanceof 类型

其中,对象是要检测的对象,类型是要检测的类名或接口名。

instanceof 运算符返回一个布尔值,如果对象是指定类型的实例,则返回 true,否则返回 false。

下面是一个示例,演示了instanceof 运算符的使用:

复制代码
Person person1 = new Student("Alice", 123);
Person person2 = new Teacher("Bob", "Math");
// 使用instanceof运算符判断对象是否是某个类的实例
System.out.println(person1 instanceof Student); // 输出: true
System.out.println(person1 instanceof Person);  // 输出: true
System.out.println(person1 instanceof Teacher); // 输出: false
System.out.println(person2 instanceof Student); // 输出: false

在上述示例中,我们创建了一个 Person 类的实例 person1,它实际上是一个 Student 对象。使用 instanceof 运算符可以判断 person1 是否是 Student 类的实例,结果为 true。同样地,我们也可以判断 person1 是否是 Person 类的实例,结果同样为 true。然而,由于 person1 并不是 Teacher 类的实例,所以返回 false。

使用 instanceof 运算符可以帮助我们在进行类型转换之前先进行类型检测,确保转换的安全性。这样可以避免类型转换异常的发生,并在需要时选择执行相应的操作。

3.2.4使用instanceof保护造型

使用 instanceof 运算符可以保护向下造型,避免 ClassCastException 异常的发生,从而减少程序中的运行错误。

在使用 instanceof 运算符进行向下造型时,可以先使用 instanceof 进行类型检测,以确保对象的类型与要转型的类型匹配。如果匹配成功,就可以进行转型操作,否则可以选择执行其他逻辑或抛出异常。

下面是一个示例,演示了如何使用 instanceof 运算符保护向下造型:

复制代码
if (person1 instanceof Student) {
    Student student = (Student) person1;
    System.out.println(student.getStudentId()); // 输出: 123
}

在上述示例中,我们先使用 instanceof 运算符检测 person1 是否是 Student 类的实例。如果匹配成功,我们就可以将 person1 强制转型为 Student 类型,并调用 getStudentId() 方法。这样可以避免在转型过程中发生 ClassCastException 异常。

此外,Java 17 引入了 instanceof 模式匹配的新特性,可以进一步简化向下转型的编码:

复制代码
if (person1 instanceof Student student) {
    System.out.println(student.getStudentId()); // 输出: 123
}

在上述示例中,我们使用 instanceof 模式匹配,将匹配成功的结果直接绑定到 student 变量上,省去了显式的类型转换操作。这样可以更加简洁地实现向下转型,并且代码更加清晰易读。

使用 instanceof 运算符和 instanceof 模式匹配可以提高程序的健壮性,确保类型转换的安全性,减少潜在的运行时错误。

4 内部类

4.1 内部类概述

4.1.1 内部类概述

内部类,顾名思义,就是声明在一个外部类内部的类。内部与外部,是一个相对的说法:外部类是指内部类所在的类。

内部类一般有以下4种分类:

  • 局部内部类:声明在外部类的局部位置上,有类名
  • 成员内部类:声明在外部类的成员位置上,有类名,无static修饰
  • 静态内部类:声明在外部类的成员位置上,有类名,有static修饰
  • 匿名内部类:在一行代码上继承父类并且创建出子类实例的语法,无类名

如下图所示:

上述4种内部类中,匿名内部类在日常开发中较为常见,将在本节中进行介绍。其他的内部类在基础的开发中较为少见,在一些特定的设计模式和框架中有所应用,将在后续的课程中进行介绍。

4.2 匿名内部类

4.2.1 什么是匿名内部类

如果在一段程序中需要创建一个类的对象(通常这个类需要实现某个接口或者继承某个类),而且对象创建后,这个类的价值也就不存在了,这个类可以不必命名,称之为匿名内部类。

利用匿名内部类可以写出简洁明快的代码,在实际开发中应用非常广泛。

语法示例如下:

4.2.2 使用匿名内部类

匿名内部类的语法在一行代码上完成了两个功能:继承父类,创建子类行对象,所以要有1个前提条件:有一个可以被继承的父类型, 这个父类型可以是类、抽象类、接口。如下所示:

利用匿名内部类,可以在一行代码上继承父类并且创建出子类实例。这样可以使用最简洁的代码实现最多的功能。由于利用匿名内部类可以写出简洁明快的代码,在实际开发中应用非常广泛。

比如存在一个父类Bird:

如果需要创建一个子类重写其move方法,对比一下普通类和匿名内部类的区别:

显然,匿名内部类显得更加简洁方便。匿名内部类简洁地省略类子类类名,也因为没有类名造成不能再复用类名创建更多地对象。在使用匿名内部类时候要注意:

1、如果只是简洁地继承父类,并且只需要创建一个子类对象,就采用匿名内部类。

2、如果子类需要反复使用创建一组子类对象就采用普通的子类。

3、匿名内部类一定是子类,一定需要有父类型时候才能使用。

4、匿名内部类的最大优点就是语法简洁,在一行上继承子类并且创建类了子类对象。

相关推荐
橙淮3 分钟前
并发编程(六)
java·jvm
拽着尾巴的鱼儿9 分钟前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影14 分钟前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
凯瑟琳.奥古斯特16 分钟前
高阶子查询题目精炼
开发语言·数据库·python·职场和发展·数据库开发
lolo大魔王20 分钟前
Linux 文件系统超全面详解(原理、结构、挂载、分区、inode、日志、管理命令)
linux·运维·服务器
喜欢踢足球的老罗28 分钟前
从移动开发转型 AI Agent 工程师:我做了一个开源学习系统
人工智能·学习
雪度娃娃34 分钟前
转向现代C++——在意为改写的函数添加 override
开发语言·c++
EntyIU1 小时前
JVM内存与GC笔记
java·jvm·笔记
wuxinyan1231 小时前
工业级大模型学习之路030:Streamlit 企业级智能体前端工作台
前端·学习·streamlit·智能体
XS0301061 小时前
并发编程 六
java·后端