(二)Java基础知识复习(面向对象编程(OOP))

382 阅读38分钟

一、类和对象

(一)面向对象思想概述

面向对象是一种编程思想,面向对象的思想更符合人类的思维模式,使用面向对象思想的好处是能将复杂的问题简单化。
面向过程:面向过程强调的是实现需求的过程,需要一步一步的去完成一个需求。
面向对象:面向对象强调的是实现需求的对象,只要能找到合适的对象,这个需求就可以不用自己实现,而是让对象去帮我们实现。

(二)类和对象概述

类,是一类具有相同属性和行为的事物的抽象。简单理解,类就是用来在程序中描述一类“事物”的。

任何事物都能够通过属性和行为进行描述,以“学生”为例:

学生的属性:姓名、性别、年龄、地址

学生的行为:学习、吃饭、睡觉

对象,是客观存在的的事物。 例如“张三同学”就是属于“学生”类的一个对象。

类和对象的关系:类是对象的抽象(描述),对象是类的实体。

(三)类的定义

class 关键字用于定义类,使用类描述一类事物,其实就是在类中定义事物具有的属性和行为。

属性:类的属性\color{red}{属性}成员变量\color{red}{成员变量}表示,成员变量是定义在类中方法外的变量。

行为:类的行为\color{red}{行为}成员方法\color{red}{成员方法}表示。

(四)对象在内存中的关系

变量存储在栈内存,对象存储在堆内存。方法的执行要入栈,方法执行结束后会出栈。多个对象存储在独立的内存空间,互不影响。多个变量指向相同对象,由于多个变量记录了同一个对象的地址值,所以通过其中任意一个变量。修改对象的信息,另一个也会受到影响。

1. 栈内存和堆内存

  • 栈内存:栈内存是用于存储局部变量、方法参数和方法调用的执行上下文的一块内存区域。它的特点是后进先出(LIFO)。当一个方法被调用时,其局部变量和方法参数会被分配到栈内存中。当方法执行结束后,这些变量会被自动销毁。
  • 堆内存:堆内存用于存储对象实例。它的特点是动态分配和释放。在堆内存中,对象的生命周期不受方法的调用和返回影响,只有当没有引用指向对象时,垃圾回收器才会回收这块内存。

2. 多个变量指向相同对象

  • 当你创建一个对象(例如一个类的实例),它会被分配到堆内存中。如果你创建了多个变量,并让它们都指向同一个对象,那么这些变量实际上都持有相同的对象引用(即对象的地址值)。
  • 因此,通过其中一个变量修改对象的信息,其他变量也会看到这些修改,因为它们都指向同一个对象。

现在,让我们来看一个简单的Java代码示例,以及对应的对象内存图:

public class Person {
    private String name;

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

    public void greet() {
        System.out.println("Hello, my name is " + name);
    }

    public static void main(String[] args) {
        Person person1 = new Person("Alice");
        Person person2 = person1; // person2指向与person1相同的对象

        person1.greet(); // 输出:Hello, my name is Alice
        person2.greet(); // 输出:Hello, my name is Alice

        // 修改person2的名字,会影响到person1
        person2.name = "Bob";

        person1.greet(); // 输出:Hello, my name is Bob
        person2.greet(); // 输出:Hello, my name is Bob
    }
}

对象内存图如下:

graph TD
  subgraph Stack
    A[person1]
    B[person2]
  end
  subgraph Heap
    C[Person]
  end
  A --> C
  B --> C

在这个示例中,person1 和 person2 都指向同一个 Person 对象。因此,无论通过哪个变量修改对象的属性,其他变量都会反映出这些变化。

(五)成员变量与局部变量的区别

image.png

二、封装

(一)封装的概述和实现

封装,是面向对象三大特性(封装、继承、多态)之一,面向对象中的封装指的是将类的实现细节隐藏在类的内部,不允许外界直接访问,而是通过该类提供的方法来实现对隐藏信息的间接访问。

(二)封装是一个比较大的概念

1. 数据的封装

概念:在 Java 中,数据封装通常是通过将类的成员变量设置为私有的,并提供公共的 getter 和 setter 方法来实现的。这确保了数据的完整性和安全性。 例子:一个 Person 类,封装了年龄属性。

public class Person {
    private String name;
    private int age; // 私有属性

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age >= 0) {
            this.age = age;
        } else {
            System.out.println("年龄不能为负数");
        }
    }

    public String getName() {
        return name;
    }

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

    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println("年龄: " + person.getAge()); // 获取年龄
        person.setAge(35); // 设置年龄
        System.out.println("新年龄: " + person.getAge()); // 获取新年龄
        person.setAge(-5); // 尝试设置无效年龄
    }
}

2. 代码的封装

概念:代码封装是将功能相关的代码逻辑封装到方法中,以提高代码的可重用性和可维护性。

例子:假设我们有一个类用来计算圆的面积和周长,可以将这部分逻辑封装在方法中。

public class CircleUtils {
    public static double calculateArea(double radius) {
        return Math.PI * radius * radius;
    }

    public static double calculateCircumference(double radius) {
        return 2 * Math.PI * radius;
    }

    public static void main(String[] args) {
        double radius = 5.0;
        double area = CircleUtils.calculateArea(radius);
        double circumference = CircleUtils.calculateCircumference(radius);
        System.out.println("圆的面积: " + area);
        System.out.println("圆的周长: " + circumference);
    }
}

3. 类的实现细节的封装

概念:在 Java 中,类的实现细节封装是通过限制对类成员的访问权限来实现的,使得类的内部工作对外部不可见,只通过公共方法与外部交互。

例子:一个 BankAccount 类,只公开存款、取款和查询余额的方法。

public class BankAccount {
    private double balance; // 私有属性

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        } else {
            System.out.println("存款金额必须为正数");
        }
    }

    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        } else {
            System.out.println("取款金额无效");
        }
    }

    public double getBalance() {
        return balance;
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount(100);
        account.deposit(50);
        account.withdraw(30);
        System.out.println("账户余额: " + account.getBalance()); // 账户余额: 120
    }
}

(三) this关键字

this 关键字的作用是区分同名的成员变量和局部变量。同时,this 还代表了当前对象。

(四)构造方法

1. 构造方法的特点

  • 构造方法的方法名和类名相同
  • 构造方法没有返回值类型,连 void 也不用写

2. 语法格式

public class Student {
    //构造方法
    public Student() {
    }
}

3. 执行时机、作用和注意事项

执行时机
每次创建类的对象,都会自动执行类的构造方法。

作用
构造方法的作用是在创建对象的同时,初始化对象的数据。

注意事项

  • 类中没有写构造方法,编译器会添加一个没有参数的构造方法。
  • 类中写了构造方法,编译器就不会添加了,如果需要空参构造,可以自己写一个。

三、继承

(一)继承概述

1.继承概述

现实生活中,继承通常指的是后人继承前人遗留的财产。而在编程语言中,继承指的是子类继承了父类的属性和行为。

现在,要编写以下两个类

  • 人类
    • 属性:姓名、年龄
    • 行为:吃饭、睡觉
  • 学生类
    • 属性:姓名、年龄
    • 行为:吃饭、睡觉、学习

我们发现,人类和学生类有一部分属性和行为是相同的,如果在两个类中都编写这些成员,代码就冗余了。

继承,就是用于解决此类问题。使用继承,能够 提升代码的复用性,减少代码冗余

2.继承的含义

继承,描述的是类与类之间的关系,一个类要继承另一个类,必须符合 is-a 的关系。

image.png 例如,兔子是食草动物的一种,食草动物是动物的一种,只有符合 is-a 的关系才能够使用继承。

3.继承的语法格式

在 Java 中,继承是通过使用 extends 关键字来实现的。继承允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以根据需要添加新的属性和方法或重写父类的方法。

a. 语法格式

下面是 Java 继承的基本语法格式:

class ParentClass {
    // 父类的属性和方法
}

class ChildClass extends ParentClass {
    // 子类的属性和方法
}

(二)子类不能继承的内容

子类不能继承父类的构造方法,因为构造方法要求必须和类名相同,如果子类继承了父类的构造方法,就违反了规则。
值得注意的是,父类中的私有成员也可以被子类继承,只是子类无法直接访问,但可以通过父类的 getter & setter 方法间接访问。

(三)继承后成员变量的访问

1. 子父类成员变量不重名,访问时没有任何影响。

class Fu {
    int num1 = 100;
}

class Zi extends Fu {
    int num2 = 200;

    public void show() {
        System.out.println(num1); //100       
        System.out.println(num2); //200    
    }
}

2. 成员变量重名

子父类成员变量重名,依据就近原则,优先访问子类的成员变量。

class Fu {    
    int num = 100;
}
class Zi extends Fu {    
    int num = 200;    
    public void show() {        
        System.out.println(num); //200        
        System.out.println(num); //200    
    }
}

3. 使用 super 关键字可以访问到父类的成员变量,super 代表父类对象的引用,this 代表的是当前对象的引用。

class Fu {    
    int num = 100;
}
class Zi extends Fu {    
    int num = 200;    
    public void show() {        
        System.out.println(super.num); //100        
        System.out.println(num); //200    
    }
}

(四)继承后成员方法的访问

1. 成员方法不重名

子父类成员方法不重名,访问时没有任何影响。

public class Demo {    
    public static void main(String[] args) {        
        Zi zi = new Zi();        
        zi.show1(); //父类的 show1 方法        
        zi.show2(); //子类的 show2 方法    
    }
}
class Fu {    
    public void show1() {        
        System.out.println("父类的 show1 方法");    
    }
}
class Zi extends Fu{    
    public void show2() {        
        System.out.println("子类的 show2 方法");    
    }
}

2. 成员方法重名

子父类成员方法重名,通过子类对象,只能访问到子类自己的成员方法。

public class Demo {    
    public static void main(String[] args) {        
        Zi zi = new Zi();        
        zi.show(); //子类的 show 方法    
    }
}
class Fu {    
    public void show() {        
    System.out.println("父类的 show 方法");    
    }
}
class Zi extends Fu{    
    public void show() {        
        System.out.println("子类的 show 方法");    
    }
}

3. 方法重写

方法重写,指的是在子父类中出现了相同的方法(返回值类型、方法名和形参都相同),会出现覆盖效果,即子类方法覆盖了父类的方法。

方法重写的作用
子类继承父类后,如果子类觉得从父类继承的方法无法满足自己的需求,可以自己写一个一模一样的方法来覆盖父类的方法。

4. @Override注解

@Override 注解的作用是告诉编译器在编译代码时,要检查方法是否符合重写规范,如果不符合则编译会失败。

class Animal {    
    public void eat() {        
        System.out.println("吃饭...");    
    }
}
class Cat extends Animal {    
    @Override    
    public void eat() {       
    System.out.println("猫吃鱼...");    
    }
}

(五)继承后构造方法的访问

1. 父类构造方法的作用

首先,构造方法的作用是在创建对象的同时初始化对象的成员变量。

在继承关系中,子类的成员变量可以通过子类自己的构造方法进行初始化,而从父类继承下来的成员变量则可以通过调用父类的构造方法进行初始化。

任何类的任何构造方法,第一行都隐含一行 super() 代码,作用是调用父类的 空参构造方法 来初始化父类中的数据。我们也可以手动的写出来,并通过 传递参数 来调用父类的 有参构造方法

需要注意的是,调用父类构造方法的代码只能写在第一行,并且,手动写了 super(参数…) 后,隐含的 super() 就没有了。

2. super 和 this 在构造方法中的使用

如果要在构造方法中调用自己其他的构造方法,可以使用 this(参数…) 去调用。

需要注意的是,this() 也必须写在构造方法第一行,并且 super() 和 this() 只能二选一。

无论是调用父类的构造方法,还是调用自己的构造方法,父类数据的初始化永远优先于子类

(六)this 和 super 关键字总结

关键字含义
this 代表了 当前对象 的引用,super 代表了 父类对象 的引用。 访问成员变量

this.成员变量;  //本类的
super.成员变量; //父类的

访问成员方法

this.成员方法();    //本类的
super.成员方法();   //父类的

访问构造方法

this();     //本类的
super();    //父类的

(七)继承体系内存图

动画.gif

(八)单继承

Java 语言只支持类的单继承,不支持类的多继承!

class A { }
class B { }
class C extends A { } //单继承,正确 
class D extends A, B { } //多继承,不支持

一个类可以有多个子类

class A { }
class B extends A { } //B 继承 A
class C extends A { } //C 继承 A

多层继承

class A { }
class B extends A { } //B 继承 A
class C extends B { } //C 继承 B

如果一个类没有继承任何类,那么它的父类默认是 Object 。也就是说,Java 中所有的类,都直接或间接继承了 Object 。

四、抽象类和接口

(一)抽象类

1. 抽象类和抽象方法概述

在某些特定的情况下,一个父类中的方法被它所有的子类都重写了,因为它的子类都有自己的特定实现,那么,这个父类方法中的代码就失去了意义。

虽然父类方法中的代码失去了意义,但方法的签名仍然有用,作用是规范子类的方法签名。即使所有子类都重写了父类的方法,这些子类的方法签名仍然和父类方法签名是一致的(方法重写规范)。

既然父类方法中的代码已经失去了意义,那么,我们是否可以只要方法签名,而不要方法体呢?

Java 允许一个方法只有方法的签名而没有方法体,这种方法被称为 抽象方法,但抽象方法只能写在 抽象类 中,这意味着,包含了抽象方法的类,必须是一个抽象类!

2. 抽象类和抽象方法的定义

定义抽象类和抽象方法都是使用 abstract 关键字。

语法格式

public abstract class 类名 {    
    //成员变量    
    //构造方法    
    //成员方法    
    //抽象方法    
    public abstract 返回值类型 方法名(形参列表 ...);
}

注意:

  • 抽象类不能创建对象!
  • 由于抽象方法没有方法体,子类在继承抽象父类后,必须将抽象父类中所有的抽象方法全部重写。
抽象类的注意事项\color{orange}{抽象类的注意事项}

a.抽象类不能创建对象:
抽象类中可能包含没有方法体的抽象方法,调用抽象方法是没有意义的,所以不允许创建抽象类的对象,避免通过对象调用抽象方法。

b.抽象类可以有构造方法:
抽象类的构造方法是提供给子类使用的,因为抽象类中仍然可以定义成员变量,子类可以通过抽象类中的构造方法初始化抽象类中的成员变量。

c.抽象类中可以没有抽象方法,但有抽象方法的类一定是抽象类:
不包含抽象方法的抽象类,唯一的目的就是禁止外界创建该类的对象,通常用于某些特殊的结构设计。

d.抽象类的子类必须重写抽象类中的所有抽象方法,除非这个子类也是抽象类:
一个类继承了抽象类,那么必须重写抽象类中的抽象方法,因为抽象方法没有方法体,必须重写后再执行才有意义。如果子类也是一个抽象类,那么可以不重写父类中的抽象方法。

3. 抽象类的意义

抽象类存在的意义就是给子类继承,否则抽象类将毫无意义。抽象类体现的是一种模板思想。

模板思想的意思是,父类只定义通用的实现,无法确定的内容则定义成抽象方法,交给子类去做具体的实现。

4. 模板设计模式

a. 模板设计模式概述

什么是设计模式? 设计模式是面向对象程序设计中一些通用问题的最佳解决方案。

什么是模板设计模式?
模板设计模式是设计模式中的一种,主要用于解决这样的一类问题:某个事件的处理流程是确定的,\color{red}{某个事件的处理流程是确定的,} 但在特定的环境下,事件处理流程中的某些步骤会有不同的表现。\color{red}{但在特定的环境下,事件处理流程中的某些步骤会有不同的表现。}

b. 下面是使用 Java 实现模板设计模式的示例。这个示例同样模拟了制作咖啡和茶的过程:
// 抽象类,表示饮料
abstract class CaffeineBeverage {
    // 模板方法,定义饮料制作的算法步骤
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    // 共用的方法
    void boilWater() {
        System.out.println("Boiling water");
    }

    void pourInCup() {
        System.out.println("Pouring into cup");
    }

    // 抽象方法,需要由子类实现
    abstract void brew();
    abstract void addCondiments();
}

// 子类实现具体的步骤
class Tea extends CaffeineBeverage {
    void brew() {
        System.out.println("Steeping the tea");
    }

    void addCondiments() {
        System.out.println("Adding lemon");
    }
}

class Coffee extends CaffeineBeverage {
    void brew() {
        System.out.println("Dripping Coffee through filter");
    }

    void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
}

// 主类,测试模板方法模式
public class BeverageTest {
    public static void main(String[] args) {
        CaffeineBeverage tea = new Tea();
        CaffeineBeverage coffee = new Coffee();

        System.out.println("Making tea:");
        tea.prepareRecipe();

        System.out.println("\nMaking coffee:");
        coffee.prepareRecipe();
    }
}

示例中:

  • CaffeineBeverage 是一个抽象类,包含一个模板方法 prepareRecipe,定义了制作饮料的通用步骤。
  • brewaddCondiments 是抽象方法,由子类 TeaCoffee 实现,以提供各自特有的实现。
  • TeaCoffee 是具体类,提供了冲泡和添加调料的具体实现。
  • BeverageTest 是测试类,通过实例化 TeaCoffee 对象,并调用 prepareRecipe 方法来展示模板方法模式的工作方式。

这种设计模式的主要优势是:它允许在不改变整体算法结构的情况下重定义特定步骤,并鼓励代码的复用和灵活性。

(二)final关键字

1. final 关键字概述

final 表示最终的,不可改变的。可用于修饰类、方法和变量。

修饰类:被 final 修饰的类不能被其他类继承

修饰方法:被 final 修饰的方法不能被子类重写

修饰变量:被 final 修饰的变量不能被重新赋值

2. final 关键字的使用

修饰类

public final class 类名 { }

修饰方法

public final 返回值类型 方法名(形参列表 ...) {    
    //方法体
}

修饰局部变量

final int number = 100;
//错误,不能重新赋值。如果变量是引用类型,则不能指向另一个对象
//number = 200;

修饰成员变量

//被 final 修饰的成员变量,要么直接赋值,要么在构造方法中赋值
private final int number = 100;

常量:被 final 修饰的变量称为 常量,常量的命名规范为所有单词的字母都是大写,单词与单词之间用下划线分隔。

//常量的命名规范
private final int MAX_VALUE = 100;

(三)static关键字

1. static 静态概述

有些时候,我们希望在类中定义一些能够被类的所有对象所共享的全局内容,这些内容仅在内存中存储一份,以便于我们对这些共享内容的管理和维护,同时也能够节省更多的内存空间。

静态,就是 Java 提供给我们解决上述问题的一种机制,使用 static 关键字修饰的成员(包括成员变量、成员方法、代码块和内部类)就是类的静态成员。静态成员最大的特点就是它属于类,而不是对象。我们可以在不创建对象的情况下,直接通过类访问静态成员。

需要注意的是,静态成员是随着类的加载而存在的,这意味着静态成员始终优先于对象存在。

静态成员的好处

  • 全局的,可以被所有对象所共享,在内存中只存储一份,更节省内存空间。
  • 可以直接通过类访问而不用创建对象,编码效率更高。

静态成员的缺点

  • 静态成员是属于类的,类不被卸载,静态成员就一直存在。而实例成员会随着对象的销毁一并被 GC 回收。

2. static 静态的使用

a. 静态变量
  • 静态变量的定义
class ChinesePeople {    
    //国籍    
    static String nationality = "China";
}
  • 静态变量的访问
public static void main(String[] args) {    
    //通过类名直接访问,推荐    
    System.out.println(ChinesePeople.nationality); 
    //China    
    ChinesePeople.nationality = "中国";    
    System.out.println(ChinesePeople.nationality); //中国    
    //通过对象访问,不推荐    
    ChinesePeople cp = new ChinesePeople();    
    System.out.println(cp.nationality); //中国
}
b. 静态方法
  • 静态方法的定义(静态方法中不能使用 this 关键字,因为 this 表示当前对象,而静态方法存在时还没有对象!)
class ChinesePeople {    
    //说话    
    public static void speak() {        
    System.out.println("说普通话");    
    }
}
  • 静态方法的访问
public static void main(String[] args) {    
    //通过类名直接调用静态方法    
    ChinesePeople.speak(); //说普通话
}
c. 代码块

使用 { } 括起来的代码称为代码块,Java 语言提供了四种代码块。

  • 局部代码块:写在方法中的代码块,作用是限制变量的作用范围。
  • 构造代码块:写在类中方法外的代码块,每次创建类的对象时都会执行,且优先于构造方法之前执行。
  • 静态代码块:使用 static 修饰的构造代码块就是静态代码块,静态代码块是类被加载时执行,且仅执行一次。
  • 同步代码块:它用于确保多个线程可以安全地访问共享资源,以防止数据的不一致性和竞争条件。同步代码块通过锁定特定对象来控制对该对象的访问。
//同步代码块的用法
//当多个线程同时访问共享资源时,如果没有适当的同步措施,就可能会发生竞态条件,这会导致数据不一
//致。同步代码块通过使用锁来确保只有一个线程可以访问代码块中的资源,从而避免这种情况。

//语法
//同步代码块的基本语法如下:


synchronized (lockObject) {
    // 需要同步的代码
}


// `lockObject` 是一个对象,用于控制对同步代码块的访问。
//当一个线程进入同步代码块时,它会锁定 `lockObject`,其他线程必须等待该线程退出代码块并释放锁后才能进入。

//示例

//以下是一个简单的例子,展示了如何使用同步代码块来确保对共享资源的线程安全访问:

class Counter {
    private int count = 0;

    // 增加计数器值的方法
    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    // 获取计数器值的方法
    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 创建多个线程,同时增加计数器
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出计数器的最终值
        System.out.println("Final count: " + counter.getCount());
    }
}

//解释

//`Counter` 类包含一个计数器变量 `count` 和一个 `increment` 方法,用于增加计数器的值。
//`increment` 方法中的同步代码块 `synchronized (this)` 确保只有一个线程可以同时修改 `count` 变量。
//在 `SynchronizedBlockExample` 类中,两个线程同时调用 `increment` 方法。
//使用 `synchronized` 关键字,确保对 `count` 变量的访问是线程安全的,避免竞态条件。

//通过使用同步代码块,开发者可以确保多线程程序中的数据一致性,并防止多个线程同时修改共享资源时可能出现的问题。
d. 静态代码块

使用 static 修饰的局部代码块就是静态代码块,静态代码块是在类被加载时执行,且仅执行一次。

  • 静态变量的定义
public class ChinesePeople {        
    //静态代码块    
    static {        
        System.out.println("静态代码块执行了...");    
    }
}

(四)接口

1. 接口概述

接口,是 Java 语言中的一种数据类型,如果说类中封装了成员变量、构造方法和成员方法,那么接口主要就是用于封装抽象方法的。和类一样,接口也会被编译成 class 字节码文件。

在 JDK8 以前,接口中只能定义 静态常量抽象方法
从 JDK8 开始,接口中还可以定义 默认方法静态方法

需要注意的是,接口不能创建对象。接口存在的意义就是给子类实现(类似于继承),一个类实现了接口,就必须要实现接口中所有的抽象方法,除非这个类是一个抽象类。

2. 接口的定义

a. 定义接口的基本格式

接口使用 interface 关键字定义。

语法格式

public interface 接口名 {        
    //常量    
    //抽象方法    
    //默认方法    
    //静态方法
}
b. 定义常量

接口中的常量必须是静态常量,支持简化格式。

public interface MyInterface {    
    //常量(完整格式)    
    public static final int MIN_VALUE = 100;    
    //常量(简化格式)    
    int MAX_VALUE = 999;
}
c. 定义抽象方法

接口中的抽象方法,支持简化格式。

public interface MyInterface {    
    //抽象方法(完整格式)    
    public abstract void method1();        
    //抽象方法(简化格式)    
    void method2();
}
d. 定义默认方法

默认方法就是有方法体的方法,支持简化格式。

public interface MyInterface {    
    //默认方法(完整格式)    
    public default void method3() {        
        System.out.println("接口中的默认方法 method3");    
    }        
    //默认方法(简化格式)    
    default void method4() {        
    System.out.println("接口中的默认方法 method4");    
    }
}
e. 定义静态方法

接口中的静态方法,支持简化格式。

public interface MyInterface {    
    //静态方法(完整格式)    
    public static void method5() {        
        System.out.println("接口中的默认方法 method5");    
    }    
    //静态方法(简化格式)    
    static void method6() {        
        System.out.println("接口中的默认方法 method6");    
    }
}

3. 接口的实现

a. 接口实现概述

类和类之间是 继承 关系,类和接口之间是 实现 关系,接口的实现使用 implements 关键字。

语法格式

public class 类名 implements 接口名 {    
}

注意事项

  • 实现类是普通类:必须实现接口中所有的抽象方法,默认方法可以选择性重写,不重写的话就是直接继承下来。
  • 实现类是抽象类:可以不重写接口中的抽象方法,因为抽象类中允许有抽象方法的存在。
b. 抽象方法的使用

类实现了接口之后,需要实现接口中所有的抽象方法。

interface MyInterface {    
    //抽象方法   
    public abstract void method1();   
    public abstract void method2();
}
class MyInterImpl implements MyInterface {    
    @Override    
    public void method1() {        
        System.out.println("实现类重写后的 method1 方法");    
        }    
    @Override    
    public void method2() {        
        System.out.println("实现类重写后的 method2 方法");    
    }
}
c. 默认方法的使用

接口中的默认方法,实现类可以选择性进行重写,如果不重写则直接继承下来。

interface MyInterface {    
    //默认方法    
    public default void method3() {        
        System.out.println("接口中的默认方法 method3");    
    }    
    public default void method4() {        
        System.out.println("接口中的默认方法 method4");    
    }
}
class MyInterImpl implements MyInterface {    
    @Override    
    public void method3() {        
        System.out.println("实现类重写后的方法 method3");    
    }
}
d. 静态方法的使用

接口中的静态方法属于接口,不能被实现类继承,所以,我们直接用接口名访问静态方法即可。

public class Demo {   
    public static void main(String[] args) {        
        //直接通过接口名访问接口中的静态方法        
        MyInterface.method5();    
    }
}
interface MyInterface {    
    //静态方法    
    public static void method5() {        
        System.out.println("接口中的静态方法 method5");    
    }
}

4. 接口的多实现

a. 接口多实现概述

类的继承体系中,一个类只能有一个父类,这指的是单继承。

对于接口,Java 语言是支持多实现的,并且可以在继承一个父类的情况下,同时实现多个接口。

语法格式

class 子类 extends 父类 implements 接口A, 接口B, 接口C {    

}
b. 抽象方法重名

多实现时,多个接口中的 抽象方法重名,实现类 仅需重写一次。

interface A {    
    public abstract void method();
}
interface B {    
    public abstract void method();
}
class C implements A, B {    
    @Override    
    public void method() {        
        System.out.println("实现类重写后的 method 方法");    
    }
}
c. 默认方法重名

多实现时,多个接口中的 默认方法重名,实现类 必须重写一次。

interface A {    
    public default void method() {        
        System.out.println("A 接口的默认方法 method");    
    }
}

interface B {    
    public default void method() {        
        System.out.println("B 接口的默认方法 method");    
    }
}

class C implements A, B {    
    @Override    
    public void method() {        
        System.out.println("实现类重写后的 method 方法");    
    }
}
d. 静态方法重名

多实现时,多个接口中的 静态方法重名,实现类 什么都不用做,因为静态方法根本无法被继承。要想访问接口中的静态方法,直接用接口名访问即可。

e. 父类成员方法和接口默认方法重名

在继承一个类的同时,实现了接口,父类中的成员方法和接口的默认方法重名,优先执行父类的成员方法

public class Demo {    
    public static void main(String[] args) {        
        Zi zi = new Zi();        
        zi.method(); //父类中的成员方法 method    
    }
}
interface A {   
    public default void method() {        
        System.out.println("A 接口的默认方法 method");    
    }
}
class Fu {    
    public void method() {        
        System.out.println("父类中的成员方法 method");    
    }
}
class Zi extends Fu implements A { }

5. 接口的多继承

a. 接口多继承概述

我们再回顾一下类、接口它们之间的关系

  • 类和类之间是继承关系,单继承,使用 extends 关键字。
  • 类和接口之间是实现关系,多实现,使用 implements 关键字。

现在,我们学习接口和接口之间的关系。

接口和接口 之间的关系是 继承 关系,使用的是 extends 关键字,并且支持接口的 多继承。

语法格式

interface A { }
interface B { }
//接口支持多继承
interface C extends A, B { }
b. 默认方法重名

接口多继承时,多个父接口中的 默认方法 重名,子接口 必须重写一次。

interface A {    
    public default void method() {        
        System.out.println("A 接口的默认方法 method");    
    }
}
interface B {    
    public default void method() {        
        System.out.println("A 接口的默认方法 method");    
    }
}
interface C extends A, B {    
    @Override    
    default void method() {        
        System.out.println("子接口重写后的 method 方法");    
    }
}

6. 接口成员总结

a. 接口中可以定义的成员:
  1. 常量

    • 使用 public static final 修饰。
    • 常量是隐式 public static final 的,因此即使不显式声明,接口中的变量默认就是常量。
  2. 抽象方法

    • 使用 public abstract 修饰。
    • 抽象方法是隐式 public abstract 的,所以可以省略这两个修饰符。
  3. 默认方法(JDK 8+):

    • 使用 public default 修饰。
    • 默认方法允许在接口中提供方法的实现。
  4. 静态方法(JDK 8+):

    • 使用 public static 修饰。
    • 静态方法可以在接口中实现,并通过接口名调用。
简化格式

在接口中,由于所有成员默认都是 public 的,可以省略 public 修饰符。以下是简化后的格式:

  • 常量

    static final int CONSTANT = 10;
    
  • 抽象方法

    void abstractMethod();
    
  • 默认方法

    default void defaultMethod() {
        // 方法实现
    }
    
  • 静态方法

    static void staticMethod() {
        // 方法实现
    }
    

在接口中,所有成员默认都是 public,所以即使不写 public 修饰符,成员仍然是公共的。使用简化格式可以使代码更简洁和易读。

b. 接口中不能定义的成员
  • 构造方法(接口不能创建对象,也没有成员变量需要初始化)
  • 静态代码块(技术上可以做到,但没有这样的需求,在接口中定义静态代码块没有意义)

(五)类、抽象类以及接口的总结

1. 综合案例

下面是一个综合案例,演示如何使用类、抽象类和接口来设计一个简单的动物园管理系统。在这个例子中,我们将定义一个动物的层次结构,包括抽象类和接口,并实现具体的动物类。

  • 抽象类

    • 封装继承体系中的共性内容。
    • 可以包含实例变量、具体方法和构造方法。
    • 用于定义类之间的继承关系,提供一个基础实现。
  • 接口

    • 提供一种扩展机制,可以用于多重实现。
    • 适合定义类与类之间的契约关系,不包含状态。
    • 可以让类实现多个接口,解决 Java 的单继承限制。
a. 设计说明
  • 接口 SoundMaker:定义动物的声音行为。
  • 接口 Mover:定义动物的移动行为。
  • 抽象类 Mammal:封装哺乳动物的共性特征。
  • 具体类 DogCat:实现 SoundMakerMover 接口,并继承 Mammal
b. 代码实现
// 定义声音行为接口
interface SoundMaker {
    String makeSound();
}

// 定义移动行为接口
interface Mover {
    String move();
}

// 抽象类,表示哺乳动物
abstract class Mammal {
    private String name;

    // 构造方法
    public Mammal(String name) {
        this.name = name;
    }

    // 获取动物名称
    public String getName() {
        return name;
    }

    // 抽象方法,描述特定哺乳动物的喂食行为
    public abstract void feed();
}

// 具体类,表示狗,继承自Mammal,实现接口
class Dog extends Mammal implements SoundMaker, Mover {
    // 构造方法
    public Dog(String name) {
        super(name);
    }

    // 实现接口方法,获取狗的声音
    @Override
    public String makeSound() {
        return "Bark";
    }

    // 实现接口方法,获取狗的移动方式
    @Override
    public String move() {
        return "Run";
    }

    // 实现抽象类方法,描述狗的喂食方式
    @Override
    public void feed() {
        System.out.println(getName() + " is eating dog food.");
    }
}

// 具体类,表示猫,继承自Mammal,实现接口
class Cat extends Mammal implements SoundMaker, Mover {
    // 构造方法
    public Cat(String name) {
        super(name);
    }

    // 实现接口方法,获取猫的声音
    @Override
    public String makeSound() {
        return "Meow";
    }

    // 实现接口方法,获取猫的移动方式
    @Override
    public String move() {
        return "Jump";
    }

    // 实现抽象类方法,描述猫的喂食方式
    @Override
    public void feed() {
        System.out.println(getName() + " is eating cat food.");
    }
}

// 测试类,展示如何使用类、抽象类和接口
public class ZooManagementSystem {
    public static void main(String[] args) {
        // 创建动物对象
        Mammal dog = new Dog("Buddy");
        Mammal cat = new Cat("Whiskers");

        // 输出动物信息
        displayAnimalInfo(dog);
        displayAnimalInfo(cat);
    }

    // 输出动物信息的方法
    public static void displayAnimalInfo(Mammal mammal) {
        System.out.println("Animal: " + mammal.getName());

        // 检查并调用声音和移动行为
        if (mammal instanceof SoundMaker) {
            System.out.println("Sound: " + ((SoundMaker) mammal).makeSound());
        }
        if (mammal instanceof Mover) {
            System.out.println("Movement: " + ((Mover) mammal).move());
        }

        mammal.feed();
        System.out.println();
    }
}
c. 解释
  • 接口 SoundMakerMover

    • 分别定义声音和移动行为,提供额外的行为扩展。
  • 抽象类 Mammal

    • 封装了哺乳动物的共性特征,如名称和喂食方式。
  • 具体类 DogCat

    • 继承自 Mammal,实现了 SoundMakerMover 接口。
    • 体现了每种动物特有的声音和移动行为。

这种设计方式通过接口实现行为的扩展,同时通过抽象类封装共性,满足了不同需求和功能的分离,更好地体现了接口的灵活性和抽象类的共性封装。

2. 总结

学到这里,相信很多萌新心中都有一个疑问,抽象类和接口那么相似,为什么不能只留下其中一个呢?
因为抽象类和接口要解决的问题是不同的

  • 抽象类封装了继承体系种的共性内容,而接口则是作为继承体系之外的扩展而存在的。
  • 由于类只支持单继承,这影响了程序的可扩展性和可维护性,而接口的出现彻底解决了 Java 语言的单继承问题。
  • 比如Lambda 表达式,它是基于接口的。

五、多态

(一)多态概述

多态,是继封装和继承之后,面向对象的第三大特性。

生活中,比如跑这个动作,小猫、小狗和大象的表现是不同的。再比如飞这个动作,昆虫、鸟类和飞机的表现也是不同的,多态描述的就是这种状态。

多态,指的是同一种行为,在不同的对象身上具有不同的表现形式。在程序中,多态指的是同一个方法,对于不同的对象具有不同的实现。

多态的前提条件

  • 继承或实现关系(二选一)
  • 方法的重写(子类重写方法才能有不同的表现形式)
  • 父类引用指向子类对象(多态的语法格式)

(二)多态的实现

多态的语法格式

父类类型 对象名 = new 子类类型();

实现多态

abstract class Animal {    
    public abstract void eat();
}

class Cat extends Animal {    
    @Override    
    public void eat() {        
        System.out.println("猫吃鱼...");    
    }
}

class Dog extends Animal {    
    @Override    
    public void eat() {        
        System.out.println("狗吃骨头...");    
    }
}
public static void main(String[] args){
    Animal cat = new Cat();
    Animal dog = new Dog();
} 

(三)多态时访问成员的特点

1. 访问成员变量和访问成员方法

  • 多态时访问成员变量,访问的是 父类 的 成员变量。(编译看父类,运行看父类)
  • 多态时访问成员方法,访问的是 子类 的 成员方法。(编译看父类,运行看子类)
a. 编译时 vs 运行时
  • 编译时:在编译阶段,Java 编译器根据引用的类型来决定可以访问哪些成员变量和方法。
  • 运行时:在运行阶段,Java 使用对象的实际类型来决定调用哪个方法实现,但成员变量的访问依然基于引用类型。

在 Java 中,多态主要体现在方法的调用上,而不是成员变量。成员变量的访问是在编译时就确定的,即访问的是引用类型的成员变量,而不是对象实际类型的成员变量。

b. 示例代码
class Parent {
    int value = 10;

    void display() {
        System.out.println("Parent display");
    }
}

class Child extends Parent {
    int value = 20;

    @Override
    void display() {
        System.out.println("Child display");
    }
}

public class Test {
    public static void main(String[] args) {
        Parent obj = new Child();

        // 访问成员变量
        System.out.println(obj.value); // 输出: 10

        // 调用方法
        obj.display(); // 输出: Child display
    }
}
c. 解释
  • 成员变量访问

    • obj.value 在编译时根据 Parent 类中的声明来确定,访问的是 Parent 类中的 value 成员变量,值为 10
    • 无论 obj 指向的是 Child 类型的实例,访问成员变量始终基于引用类型 Parent
  • 方法调用

    • obj.display() 使用了多态,调用的是 Child 类中的 display 方法,因为在运行时,Java 根据对象的实际类型(即 Child)来决定调用哪个方法。

总结

  • 成员变量:访问是静态绑定的,即在编译时已经确定,基于引用类型。
  • 方法调用:是动态绑定的,即在运行时根据实际对象类型来决定。

因此,这就是为什么在多态情况下,访问成员变量时,访问的是父类的成员变量,编译和运行都只看父类。

2. 访问静态方法

多态时访问静态方法,访问的是 父类 的 静态方法。(编译看父类,运行看父类)

在 Java 中,静态方法与类关联,而不是与实例关联。这意味着静态方法是按类来调用的,不涉及多态。即使对象变量被类型化为某个父类,静态方法的调用仍然基于引用类型,而不是实际对象类型。

静态方法在编译时解析,而不是在运行时。因此,静态方法的调用是基于引用类型来进行的。这意味着:

  • 静态方法不支持多态。
  • 调用静态方法时,看的是引用类型,而不是实际对象类型。
a. 示例代码
class Parent {
    static void staticMethod() {
        System.out.println("Parent static method");
    }

    void instanceMethod() {
        System.out.println("Parent instance method");
    }
}

class Child extends Parent {
    static void staticMethod() {
        System.out.println("Child static method");
    }

    @Override
    void instanceMethod() {
        System.out.println("Child instance method");
    }
}

public class Test {
    public static void main(String[] args) {
        Parent obj = new Child();

        // 调用静态方法
        obj.staticMethod(); // 输出: Parent static method

        // 调用实例方法
        obj.instanceMethod(); // 输出: Child instance method
    }
}
b. 解释
  • 静态方法调用

    • obj.staticMethod() 在编译时解析,基于引用类型 Parent,因此调用的是 Parent 类的 staticMethod
    • 静态方法的调用不依赖于实例,属于类级别。
  • 实例方法调用

    • obj.instanceMethod() 使用多态,在运行时基于对象的实际类型 Child 调用方法。
c. 总结
  • 静态方法调用是静态绑定的,编译时解析,基于引用类型。
  • 实例方法调用是动态绑定的,运行时解析,基于对象的实际类型。
  • 在多态情况下,访问静态方法时,编译和运行都看的是父类的静态方法。

(四)多态的几种表现形式

1. 普通父类多态

public class Demo {    
    public static void main(String[] args) {        
    //普通父类多态        
    Fu fu = new Zi();   
    }
}

class Fu {}

class Zi extends Fu {}

2. 抽象父类多态

public class Demo {    
    public static void main(String[] args) {        
    //抽象父类多态        
    Fu fu = new Zi();    
    }
}

abstract class Fu {}

class Zi extends Fu {}

3. 接口多态

public class Demo {    
    public static void main(String[] args) {        
        //多态        
        A a = new AImpl();    
    }
}

interface A {}

class AImpl implements A {}

(五)多态的应用场景

1. 变量多态

abstract class Animal {    
    public abstract void eat();
}

class Cat extends Animal {    
    @Override    
    public void eat() {        
        System.out.println("猫吃鱼...");    
    }
}

class Dog extends Animal {    
    @Override    
    public void eat() {        
        System.out.println("狗吃骨头...");    
    }
}

public static void main(String[] args) {    
    //变量多态    
    Animal a = new Cat(); //new Dog();    
    a.eat();
}

2. 参数多态

public class Demo {
    public static void main(String[] args) {        
        //参数多态        
        invoke(new Cat()); 
        //invoke(new Dog());    
    }        
    public static void invoke(Animal a) {        
        // ...    
    }
}

3. 返回值多态

public class Demo {    
    public static void main(String[] args) {        
        Animal a = createAnimal();    
    }    
    public static Animal createAnimal() {        
        //返回值多态        
        return new Cat();
        //return new Dog();    
    }
}

(六)多态的好处和弊端

多态的好处是提升了代码的扩展性和复用性

  • 扩展性:以返回值多态为例,如果有新的子类出现,方法的返回值类型和方法的调用者无需做任何修改。
  • 复用性:以参数多态为例,调用方法可以传递该类及其所有子类对象,无需针对多个类编写多个方法。

多态的弊端是无法调用子类特有的方法。

(七)引用类型转换

向上转型,多态的语法格式就是向上转型,指的是将子类对象赋值给父类类型。

//向上转型
父类类型 对象名 = new 子类类型();

要解决多态的弊端(无法访问子类特有的方法),可以使用向下转型,将父类类型的对象强转为子类类型。

//向下转型,需要强制类型转换
子类类型 对象名 = (子类类型)父类类型的对象;

六、内部类

(一)内部类

1. 内部类概述

内部类,指的是将 A 类定义在 B 类的内部,A 类称为内部类,B 类称为外部类。

语法格式

class 外部类 {    
    class 内部类 {    
    }
}

若一个事物的内部还包含其他事物,就可以使用内部类来实现。比如,汽车类中还包含发动机类,此时,可以将发动机类以内部类的形式定义在汽车类的内部。

class Car { 
    //汽车类    
    class Engine { 
        //发动机类    
    }
}

2. 在外部类中使用内部类

在外部类中使用内部类,创建内部类对象的语法和以前创建对象没有区别。

class Outer {        
    public void method() {        
        //创建内部类对象        
        Inner inner = new Inner();        
        inner.show();    
    }    
    class Inner {        
        public void show() {            
            System.out.println("内部类的 show 方法执行了...");
        }    
    }
}

3. 在其他类中使用内部类

在其他类中使用内部类,创建内部类对象的语法是不一样的。

外部类.内部类 对象名 = new 外部类().new 内部类();
public class Demo {    
    public static void main(String[] args) {        
        Outer.Inner inner = new Outer().new Inner();        
        inner.show();    
    }
}
class Outer {    
    class Inner {       
        public void show() {            
            System.out.println("内部类的 show 方法执行了...");        
        }    
    }
}

(二)匿名内部类

1. 匿名内部类概述

匿名内部类是一种创建对象的方式,能够帮助我们在没有子类或实现类的情况下,临时创建一个子类或实现类对象。

匿名内部类体现的是一种临时的思想,如果不想单独写一个子类或实现类,那么使用匿名内部类是一个不错的选择。

语法格式

父类类型 对象名 = new 父类类型() {        
    @Override    
    public void 方法名() {        
        System.out.println("匿名内部类的临时实现...");    
    }
};

2. 匿名内部类的应用场景

匿名内部类的应用场景包括

  • 将匿名内部类对象赋值给一个 变量
  • 将匿名内部类对象作为 参数 进行传递
  • 将匿名内部类对象作为方法的 返回值

匿名内部类在 Java 中是一种特殊的类,没有显式的类名,可以用来简化代码,特别是在需要快速创建类的实例而不需要复用的场景。以下是匿名内部类在不同应用场景中的示例代码。

a. 将匿名内部类对象赋值给一个变量
interface Greeting {
    void sayHello();
}

public class AnonymousInnerClassExample {
    public static void main(String[] args) {
        // 将匿名内部类对象赋值给变量
        Greeting greeting = new Greeting() {
            @Override
            public void sayHello() {
                System.out.println("Hello, World!");
            }
        };

        greeting.sayHello(); // 输出: Hello, World!
    }
}
b. 将匿名内部类对象作为参数进行传递
interface Calculator {
    int calculate(int a, int b);
}

public class AnonymousInnerClassExample {
    public static void main(String[] args) {
        // 调用方法时传递匿名内部类对象作为参数
        performOperation(5, 3, new Calculator() {
            @Override
            public int calculate(int a, int b) {
                return a + b;
            }
        }); // 输出: Result: 8
    }

    public static void performOperation(int a, int b, Calculator calculator) {
        int result = calculator.calculate(a, b);
        System.out.println("Result: " + result);
    }
}
c. 将匿名内部类对象作为方法的返回值
interface Formatter {
    String format(String message);
}

public class AnonymousInnerClassExample {
    public static void main(String[] args) {
        Formatter formatter = getFormatter();
        String formattedMessage = formatter.format("Hello, Java!");
        System.out.println(formattedMessage); // 输出: [Formatted] Hello, Java!
    }

    public static Formatter getFormatter() {
        // 返回匿名内部类对象
        return new Formatter() {
            @Override
            public String format(String message) {
                return "[Formatted] " + message;
            }
        };
    }
}
d. 总结
  • 赋值给变量:在需要单次实现接口的情况下,匿名内部类可以用来快速创建实例。
  • 作为参数传递:适合在需要立即使用接口实例的场合,常见于事件处理和回调。
  • 作为返回值:用于提供接口的实现,简化返回的类结构,而不需要单独定义类。