掘金 后端 ( ) • 2024-04-16 13:44

当谈到Java中的类和对象时,类是一种抽象的模板,用于描述具有相同属性和行为的对象的集合。而对象是类的一个具体实例,它具有类定义的属性和行为。

1. 定义类

在Java中,类通过class关键字来定义,类定义包含了类名、类的属性和方法。

语法规则:

class ClassName {
    // 类的属性
    dataType attributeName;
    
    // 类的方法
    returnType methodName(parameterList) {
        // 方法体
    }
}

示例代码:

class Car {
    // 类的属性
    String color;
    int speed;
    
    // 类的方法
    void drive() {
        System.out.println("Driving at speed: " + speed);
    }
}

2. 创建和使用类

要创建类的对象,使用new关键字并调用类的构造函数。一旦创建了对象,就可以使用点.运算符来访问对象的属性和调用对象的方法。

语法规则:

ClassName objectName = new ClassName();

示例代码:

public class Main {
    public static void main(String[] args) {
        // 创建Car类的对象
        Car myCar = new Car();
        
        // 设置对象的属性
        myCar.color = "Red";
        myCar.speed = 60;
        
        // 调用对象的方法
        myCar.drive();
    }
}

实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值。

数据类型 默认值 byte 0 short 0 int 0 long 0L float 0.0f double 0.0d char '\u0000' boolean false 引用类型 null 类 = 属性 + 方法。属性:描述的是状态。方法:描述的是行为。我们通常描述一个对象的行为动作时,不加static。没有添加static的方法,被叫做:实例方法。(对象方法)。实例变量要想访问,必须先new对象。通过引用来访问实例变量。实例变量是不能通过类名直接访问的。同样地,实例方法不能使用“类名.”去调用。
public class Vip {  
    // 类 = 属性 + 方法  
    // 属性:描述的是状态。  
    // 方法:描述的是行为。  
  
    // 姓名  
    String name; // 实例变量(对象变量)  
    // 年龄  
    int age; // 实例变量(对象变量)  
  
    // 会员行为动作  
    // 购物行为  
    // 先记住:我们通常描述一个对象的行为动作时,不加static。  
    // 没有添加static的方法,被叫做:实例方法。(对象方法)  
    public void shopping(){  
        System.out.println("正在购物!");  
    }  
}
public class VipTest01 {  
    public static void main(String[] args) {  
        // 创建一个Vip对象  
        Vip vip1 = new Vip();  
  
        // 给name和age属性赋值  
        vip1.name = "jack";  
        vip1.age = 20;  
        System.out.println("name = " + vip1.name);  
        System.out.println("age = " + vip1.age);  
        // 去购物  
        vip1.shopping();  
  
        // 再创建一个Vip对象  
        Vip vip2 = new Vip();  
        vip2.name = "lisi";  
        vip2.age = 15;  
        System.out.println("name = " + vip2.name);  
        System.out.println("age = " + vip2.age);  
        // 去购物  
        vip2.shopping();  
  
        // 为什么name和age不能使用“类名.”访问。  
        // 实例变量要想访问,必须先new对象。通过引用来访问实例变量。  
        // 实例变量是不能通过类名直接访问的。  
        //System.out.println(Vip.name);  
        //System.out.println(Vip.age);  
        // 编译报错。实例方法不能使用“类名.”去调用。  
        //Vip.shopping();  
  
    }  
}

2.1方法参数传递的内存分析

定义User

public class User {  
    int age;  
}

基本数据类型:

public class ArgsTest01 {  
    public static void main(String[] args) {  
        int i = 10;  
        // 调用add方法的时候,将i传进去,实际上是怎么传的?将i变量中保存值10复制了一份,传给了add方法。  
        add(i);  
        System.out.println("main--->" + i); // 10  
    }  
    public static void add(int i){ // 方法的形参是局部变量。  
        i++;  
        System.out.println("add--->" + i); // 11  
    }  
}

image.png

引用数据类型:

public class ArgsTest02 {  
    public static void main(String[] args) {  
        User u = new User();  
        u.age = 10;  
        // u是怎么传递过去的。实际上和i原理相同:都是将变量中保存的值传递过去。  
        // 只不过这里的u变量中保存的值比较特殊,是一个对象的内存地址。  
        add(u);  
        System.out.println("main-->" + u.age); // 11  
    }  
  
    public static void add(User u) { // u是一个引用。  
        u.age++;  
        System.out.println("add-->" + u.age); // 11  
    }  
}

image.png

3.初识this关键字

this 本质上是一个引用。保存的是当前对象的内存地址。

public class Student {  
    String name;  
    public void study(){  
        //System.out.println(this.name + "正在努力的学习!");  
        // this. 是可以省略。默认访问的就是当前对象的name。  
        System.out.println(name + "正在努力的学习!!!!!!!");  
    }  
}
public class StudentTest {  
    public static void main(String[] args) {  
        Student zhangsan = new Student();  
        zhangsan.name = "张三";  
        // 让张三去学习。  
        zhangsan.study();  
          
        Student lisi = new Student();  
        lisi.name = "李四";  
        // 让李四去学习。  
        lisi.study();  
    }  
}

4.OOP三大特征之封装

4.1为什么使用封装?

先不使用封装机制,分析程序存在哪些问题?

public class User {  
    int age;  
}
public class UserTest {  
    public static void main(String[] args) {  
        User user = new User();  
        System.out.println("年龄:" + user.age); // 0  
        user.age = 50;  
        System.out.println("年龄:" + user.age); // 50  
  
        // 目前User类没有进行封装,在外部程序中可以对User对象的age属性进行随意的访问。  
        // 这样非常不安全的。(因为现实世界中age不可能是负数。如果是真正的业务,-100不应该能够赋值给age变量。)  
        user.age = -100;  
  
        System.out.println("年龄:" + user.age); // -100  
    }  
}

User类型对象的age属性非常不安全。在外部程序中可以对其随意的访问。

为了保证User类型对象的age属性的安全,我们需要使用封装机制。

4.2实现封装的步骤:

  1. 第一步:属性私有化。(什么是私有化?使用 private 进行修饰。) 属性私有化的作用是:禁止外部程序对该属性进行随意的访问。所有被private修饰的,都是私有的,私有的只能在本类中访问。
  2. 第二步:对外提供setter和getter方法。 为了保证外部的程序仍然可以访问age属性,因此要对外提供公开的访问入口。那么应该对外提供两个方法,一个负责读,一个负责修改。 读方法的格式:public int getAge(){}。getter方法是绝对安全的。因为这个方法是读取属性的值,不会涉及修改操作。 改方法的格式:public void setAge(int age){}。setter方法当中就需要编写拦截过滤代码,来保证属性的安全。
public class User {  
    private int age;  
  
    public int getAge(){  
        //return this.age;  
        return age;  
    }  
  
    /*public void setAge(int num){  
        //this.age = num;  
        if(num < 0 || num > 100) {  
            System.out.println("对不起,您的年龄值不合法!");  
            return;  
        }  
        age = num;  
    }*/  
  
    // java有就近原则。  
    public void setAge(int age){  
        if(age < 0 || age > 100) {  
            System.out.println("对不起,您的年龄值不合法!");  
            return;  
        }  
        // this. 大部分情况下可以省略。  
        // this. 什么时候不能省略?用来区分局部变量和实例变量的时候。  
        this.age = age;  
    }  
}

为了使setAge方法的参数更加符合实际意义。所以要用age,但是age又和成员变量冲突。需要用this关键字来进行区分。

4.3实例方法调用实例方法

image.png

调用方法是需要引用的,但是我们常常省略了this关键字的引用。

5.构造方法(Constructor)

  1. 构造方法有什么作用? 作用1:对象的创建(通过调用构造方法可以完成对象的创建)。 作用2:对象的初始化(给对象的所有属性赋值就是对象的初始化)。
  2. 怎么定义构造方法呢?
[修饰符列表] 构造方法名(形参列表){  
	构造方法体;  
}
  1. 构造方法怎么调用呢? 语法:new 构造方法名(实参); 注意:构造方法最终执行结束之后,会自动将创建的对象的内存地址返回。但构造方法体中不需要提供“return 值;”这样的语句。

  2. 在java语言中,如果一个类没有显示的去定义构造方法,系统会默认提供一个无参数的构造方法。(通常把这个构造方法叫做缺省构造器。)

  3. 一个类中如果显示的定义了构造方法,系统则不再提供缺省构造器。所以,为了对象创建更加方便,建议把无参数构造方法手动的写出来。

  4. 在java中,一个类中可以定义多个构造方法,而且这些构造方法自动构成了方法的重载(overload)。

  5. 构造方法中给属性赋值了?为什么还需要单独定义set方法给属性赋值呢? 在构造方法中赋值是对象第一次创建时属性赋的值。set方法可以在后期的时候调用,来完成属性值的修改。

  6. 构造方法执行原理? 构造方法执行包括两个重要的阶段:

    • 第一阶段:对象的创建
    • 第二阶段:对象的初始化 这两个阶段不可颠倒,不可分割。 对象在什么时候创建的? new的时候,会直接在堆内存中开辟空间。然后给所有属性赋默认值,完成对象的创建。(这个过程是在构造方法体执行之前就完成了。)
  7. 构造代码块 格式:{}包裹即可。

    构造代码块什么时候执行,执行几次? 每一次在new的时候,都会先执行一次构造代码块。构造代码块是在构造方法执行之前执行的。 构造代码块有什么用? 如果所有的构造方法在最开始的时候有相同的一部分代码,不妨将这个公共的代码提取到构造代码块当中,这样代码可以得到复用。

理解构造方法和构造代码块对于理解对象的创建和初始化过程非常重要。首先,我们来定义一个简单的类 Car,并演示构造方法的定义、调用和作用:

public class Car {
    private String brand;
    private String color;
    private int speed;

    // 构造方法1:无参数构造方法(缺省构造器)
    public Car() {
        System.out.println("无参数构造方法被调用");
        // 可以在构造方法中进行初始化工作
        brand = "Unknown";
        color = "Unknown";
        speed = 0;
    }

    // 构造方法2:带参数的构造方法
    public Car(String brand, String color, int speed) {
        System.out.println("带参数构造方法被调用");
        // 可以在构造方法中进行初始化工作
        this.brand = brand;
        this.color = color;
        this.speed = speed;
    }

    // 构造代码块
    {
        System.out.println("构造代码块被执行");
        // 可以在构造代码块中执行初始化工作
        // 这部分代码会在每次对象创建时执行
        // 这里能够使用this,这说明,构造代码块执行之前对象已经创建好了,并且系统也完成了默认赋值。  
		System.out.println(this.speed);
    }

    // 演示构造方法的调用和对象的创建
    public static void main(String[] args) {
        // 调用无参数构造方法创建对象
        Car car1 = new Car();
        System.out.println("品牌:" + car1.brand + ",颜色:" + car1.color + ",速度:" + car1.speed);

        // 调用带参数构造方法创建对象
        Car car2 = new Car("Toyota", "Blue", 60);
        System.out.println("品牌:" + car2.brand + ",颜色:" + car2.color + ",速度:" + car2.speed);
    }
}

上述代码演示了构造方法的定义和调用。其中,无参数构造方法用于创建对象时进行默认初始化,而带参数的构造方法允许在创建对象时传入特定的属性值。

接下来,我们来看一下构造代码块的示例:

public class MyClass {
    // 构造代码块
    {
        System.out.println("构造代码块被执行");
        // 这里可以进行初始化工作
    }

    // 构造方法
    public MyClass() {
        System.out.println("构造方法被调用");
    }

    public static void main(String[] args) {
        MyClass obj1 = new MyClass();
        MyClass obj2 = new MyClass();
    }
}

在这个示例中,构造代码块会在每次对象创建时执行,用于执行初始化工作。无论是通过哪个构造方法创建对象,构造代码块都会在构造方法之前执行。

6.this关键字进阶

image.png

this代表的是当前对象。static的方法中没有当前对象。所以static的方法中不能使用this。

image.png

this用法总结:

  1. 引用当前对象的成员变量:通过this关键字可以引用当前对象的成员变量,尤其在成员变量与局部变量同名的情况下,可以使用this关键字明确指定成员变量。

  2. 引用当前对象的方法:在非静态方法中,可以使用this关键字来调用当前对象的其他方法,使代码更加清晰。

  3. 在构造方法中调用其他构造方法:通过this()语法可以在构造方法中调用当前类的其他构造方法,以避免代码重复。

6.1. 引用当前对象的成员变量:

public class Example {
    private int number;

    public Example(int number) {
        // 使用this引用当前对象的成员变量
        this.number = number;
    }

    public void printNumber() {
        // 使用this引用当前对象的成员变量
        System.out.println("Number: " + this.number);
    }

    public static void main(String[] args) {
        Example example = new Example(10);
        example.printNumber();
    }
}

6.2. 引用当前对象的方法:

public class Example {
    private String message;

    public Example(String message) {
        this.message = message;
    }

    public void displayMessage() {
        System.out.println("Message: " + this.message);
    }

    public void showMessage() {
        // 使用this引用当前对象的方法
        this.displayMessage();
    }

    public static void main(String[] args) {
        Example example = new Example("Hello");
        example.showMessage();
    }
}

6.3. 在构造方法中调用其他构造方法:

public class Example {
    private int number;
    private String message;

    public Example() {
        // 调用带参数的构造方法
        this(0, "Default");
    }

    public Example(int number, String message) {
        this.number = number;
        this.message = message;
    }

    public void printInfo() {
        System.out.println("Number: " + this.number + ", Message: " + this.message);
    }

    public static void main(String[] args) {
        Example example1 = new Example();
        example1.printInfo();

        Example example2 = new Example(10, "Hello");
        example2.printInfo();
    }
}

7.static关键字

  1. static翻译为静态的
  2. static修饰的变量:静态变量
  3. static修饰的方法:静态方法
  4. 所有static修饰的,访问的时候,直接采用“类名.”,不需要new对象。
  5. 什么情况下把成员变量定义为静态成员变量? 当一个属性是对象级别的,这个属性通常定义为实例变量。(实例变量是一个对象一份。100个对象就应该有100个空间) 当一个属性是类级别的(所有对象都有这个属性,并且这个属性的值是一样的),建议将其定义为静态变量,在内存空间上只有一份。节省内存开销。 这种类级别的属性,不需要new对象,直接通过类名访问。
  6. 静态变量存储在哪里?静态变量在什么时候初始化?(什么时候开辟空间) JDK8之后:静态变量存储在堆内存当中。 类加载时初始化。
public class Chinese {  
    String idCard;  
    String name;  
    static String country = "中国";  
  
    public Chinese(String idCard, String name) {  
        this.idCard = idCard;  
        this.name = name;  
    }  
  
    public void display() {  
        System.out.println("身份证号:" + this.idCard + ",姓名:" + this.name + ",国籍:" + this.country);  
    }  
  
    public static void test() {  
        System.out.println("静态方法test执行了");  
  
        // 这个不行  
        //display();  
        //System.out.println(name);  
        // 这些可以  
        System.out.println(Chinese.country);  
        System.out.println(country); // 在同一个类中,类名. 可以省略。  
  
        Chinese.test2();  
        test2();  
    }  
  
    public static void test2() {  
  
    }  
}

image.png

7.1静态代码块

静态代码块是在类加载时执行的特殊代码块,用于在类加载时进行一些初始化操作。下面是关于静态代码块的详细讲解:

7.1.1 语法格式:

static {
    // 静态代码块中的代码
}

7.1.2 执行时机和次数:

  • 静态代码块在类加载时执行,且只执行一次。即使没有创建对象,静态代码块也会执行。

7.1.3 可以编写多个静态代码块:

  • 静态代码块可以按照自上而下的顺序依次执行。

7.1.4. 使用场景:

  • 当需要在类加载时执行一些特定的操作时,可以将代码写入静态代码块中。
  • 典型的应用场景包括:初始化静态变量、加载驱动程序、记录日志等。
public class StaticTest01 {

    // 实例方法
    public void doSome() {
        System.out.println(name);
    }

    // 实例变量
    String name = "zhangsan";

    // 静态变量
    static int i = 100;

    // 静态代码块1
    static {
        // 在静态代码块中,可以直接访问静态变量,但不能访问实例变量。
        System.out.println(i); // 输出:100
        System.out.println("静态代码块1执行了");
        System.out.println("xxxx-xx-xx xx:xx:xx 000 -> StaticTest01.class完成了类加载!");
    }

    // 静态变量
    static int j = 10000;

    // 静态代码块2
    static {
        System.out.println("静态代码块2执行了");
    }

    public static void main(String[] args) {
        System.out.println("main execute!");
    }

    // 静态代码块3
    static {
        System.out.println("静态代码块3执行了");
    }
}

执行过程:

  1. 类加载器加载 StaticTest01 类。
  2. 静态变量 i 被初始化为 100
  3. 第一个静态代码块执行,输出 "静态代码块1执行了",并执行其中的代码。
  4. 静态变量 j 被初始化为 10000
  5. 第二个静态代码块执行,输出 "静态代码块2执行了"
  6. main 方法被调用,程序执行结束。

静态代码块在类加载时执行,并且只执行一次。它可以用于执行类的初始化操作,例如初始化静态变量、加载驱动程序等。

8.单例模式

单例模式是一种设计模式,确保类只有一个实例,并提供全局访问点。

8.1饿汉式单例模式

实现步骤:

  1. 私有化构造方法:防止外部直接通过构造方法创建对象。
  2. 提供静态方法获取实例:通过一个静态方法返回该类的实例。
  3. 定义静态变量并初始化:在类加载时初始化静态变量,确保只有一个实例存在。
// Singleton.java
public class Singleton {
    // 第三步:定义静态变量,在类加载的时候,初始化静态变量。(只初始化一次)
    private static Singleton instance = new Singleton();

    // 第一步:构造方法私有化,防止外部直接通过构造方法创建对象。
    private Singleton() {}

    // 第二步:提供一个静态方法获取实例。
    public static Singleton getInstance() {
        return instance;
    }
}

// SingletonTest01.java
public class SingletonTest01 {
    public static void main(String[] args) {
        // 通过静态方法获取实例
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        Singleton s3 = Singleton.getInstance();

        // 判断是否为同一个实例
        System.out.println(s1 == s2); // true
        System.out.println(s3 == s2); // true
    }
}

执行过程:

  1. 类加载器加载 Singleton 类。
  2. 静态变量 instance 被初始化为单例实例。
  3. main 方法中通过静态方法 Singleton.getInstance() 获取实例。
  4. 由于静态变量 instance 在类加载时就已经初始化,因此返回的是同一个实例。
  5. 判断 s1 == s2s3 == s2,都为 true,说明它们是同一个实例。

注意事项:

  • 饿汉式单例模式在类加载时就创建了实例,可能会造成资源浪费。
  • 在多线程环境下,需考虑线程安全问题。
  • 饿汉式单例模式:类加载时对象就创建好了。不管这个对象用还是不用。提前先把对象创建好。

这段代码展示了懒汉式单例模式的实现。下面是代码的解释和说明:

8.2懒汉式单例模式。

实现步骤:

  1. 私有化构造方法:防止外部直接通过构造方法创建对象。
  2. 提供静态方法获取实例:通过一个静态方法返回该类的实例。
  3. 定义静态变量但不初始化:静态变量 s 初始值为 null,在第一次调用时才创建对象。
public class Singleton {
    // 第三步:提供一个静态变量,但是这个变量值为null。
    private static Singleton instance;

    // 第一步:构造方法私有化,防止外部直接通过构造方法创建对象。
    private Singleton() {}

    // 第二步:提供一个静态方法获取实例。
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
            System.out.println("对象创建了"); // 只会在第一次创建对象时输出
        }
        return instance;
    }
}

执行过程:

  1. 第一次调用 getInstance() 方法时,instancenull,创建一个新的 Singleton 对象并赋值给 instance
  2. 后续再次调用 getInstance() 方法时,由于 instance 已经不为 null,直接返回之前创建的实例。

注意事项:

  • 懒汉式单例模式延迟了对象的创建时间,只有在第一次使用时才创建对象,节省了资源。
  • 在多线程环境下,需要考虑线程安全问题,可以通过加锁来解决。

9.OOP三大特征之继承

在面向对象编程中,继承是一种重要的概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。下面是关于继承的详细讲解:

9.1 定义继承关系:

  • 在 Java 中,使用关键字 extends 来定义一个类继承另一个类。
  • 子类(派生类)继承了父类(基类)的属性和方法。
// Person.java
public class Person {
    String name;
    int age;
    boolean sex;

    // 省略 getter 和 setter 方法

    public void eat(){
        System.out.println(name + "正在吃饭");
    }

    public void run(){
        System.out.println(name + "正在跑步,锻炼身体");
    }
}

// Student.java
public class Student extends Person{
    String course;

    // 省略 getter 和 setter 方法

    public void study(){
        System.out.println(this.name + "正在努力的学习!");
    }
}

// Teacher.java
public class Teacher extends Person{
    double sal;

    // 省略 getter 和 setter 方法

    public void teach(){
        System.out.println(name + "正在认真的授课");
    }
}

执行过程:

  1. Student 类和 Teacher 类都使用 extends 关键字继承了 Person 类。
  2. 因此,Student 类和 Teacher 类都拥有了 Person 类中的 nameagesex 属性,以及 eat()run() 方法。
  3. Student 类额外添加了 course 属性和 study() 方法。
  4. Teacher 类额外添加了 sal 属性和 teach() 方法。

Java不支持多继承,不能同时直接继承多个类。只能“直接”继承1个类。单继承。 image.png

子类不能继承父类的构造方法。值得注意的是子类可以继承父类的私有成员(成员变量,方法),只是子类无法直接访问而已,可以通过getter/setter方法访问父类的private成员变量。

public class Demo03 {
    public static void main(String[] args) {
        Zi z = new Zi();
        System.out.println(z.num1);
//		System.out.println(z.num2); // 私有的子类无法使用
        // 通过getter/setter方法访问父类的private成员变量
        System.out.println(z.getNum2());

        z.show1();
        // z.show2(); // 私有的子类无法使用
    }
}

class Fu {
    public int num1 = 10;
    private int num2 = 20;

    public void show1() {
        System.out.println("show1");
    }

    private void show2() {
        System.out.println("show2");
    }

    public int getNum2() {
        return num2;
    }

    public void setNum2(int num2) {
        this.num2 = num2;
    }
}

class Zi extends Fu {
}

9.2默认继承根类 Object

  • 如果一个类没有显式地继承任何类,那么它默认继承根类 Object
  • Object 是 Java JDK 类库中的根类,位于 java.lang 包中。
class E {
    // 类 E 没有显式地继承任何类,因此默认继承 Object 类。
}

class F extends E {
    // 类 F 继承了类 E。
}

public class Test2 {
    public static void main(String[] args) {
        // 创建 E 类的对象
        E e = new E();
        // 因为 E 类默认继承 Object,所以可以调用 Object 类中的方法
        // 打印 e 对象的字符串表示形式
        String s = e.toString();
        System.out.println(s);

        // 创建 F 类的对象
        F f = new F();
        // 因为 F 类继承了 E 类,而 E 类继承了 Object 类,所以也可以调用 Object 类中的方法
        // 打印 f 对象的字符串表示形式
        System.out.println(f.toString());
    }
}

输出结果:

com.rainsoul.oop.oop17.E@2d98a335
com.rainsoul.oop.oop17.F@6996dbf2
  • 因为类 E 和类 F 都继承了 Object 类,所以它们都可以调用 Object 类中定义的方法,比如 toString() 方法。
  • 默认情况下,toString() 方法返回的是对象的类名和内存地址的十六进制表示形式。

9.4继承后的特点—成员变量

9.4.1成员变量不重名

如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。代码如下:

class Fu {
	// Fu中的成员变量
	int num = 5;
}
class Zi extends Fu {
	// Zi中的成员变量
	int num2 = 6;
  
	// Zi中的成员方法
	public void show() {
		// 访问父类中的num
		System.out.println("Fu num="+num); // 继承而来,所以直接访问。
		// 访问子类中的num2
		System.out.println("Zi num2="+num2);
	}
}
class Demo04 {
	public static void main(String[] args) {
        // 创建子类对象
		Zi z = new Zi(); 
      	// 调用子类中的show方法
		z.show();  
	}
}

演示结果:
Fu num = 5
Zi num2 = 6

9.4.2成员变量重名

如果子类父类中出现重名的成员变量,这时的访问是有影响的。代码如下:

class Fu1 {
	// Fu中的成员变量。
	int num = 5;
}
class Zi1 extends Fu1 {
	// Zi中的成员变量
	int num = 6;
  
	public void show() {
		// 访问父类中的num
		System.out.println("Fu num=" + num);
		// 访问子类中的num
		System.out.println("Zi num=" + num);
	}
}
class Demo04 {
	public static void main(String[] args) {
      	// 创建子类对象
		Zi1 z = new Zi1(); 
      	// 调用子类中的show方法
		z1.show(); 
	}
}
演示结果:
Fu num = 6
Zi num = 6

子父类中出现了同名的成员变量时,子类会优先访问自己对象中的成员变量。如果此时想访问父类成员变量如何解决呢?我们可以使用super关键字。子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用super 关键字,修饰父类成员变量,类似于之前学过的 this

需要注意的是:super代表的是父类对象的引用,this代表的是当前对象的引用。

class Fu {
	// Fu中的成员变量。
	int num = 5;
}

class Zi extends Fu {
	// Zi中的成员变量
	int num = 6;
  
	public void show() {
        int num = 1;
      
        // 访问方法中的num
        System.out.println("method num=" + num);
        // 访问子类中的num
        System.out.println("Zi num=" + this.num);
        // 访问父类中的num
        System.out.println("Fu num=" + super.num);
	}
}

class Demo04 {
	public static void main(String[] args) {
      	// 创建子类对象
		Zi1 z = new Zi1(); 
      	// 调用子类中的show方法
		z1.show(); 
	}
}

演示结果:
method num=1
Zi num=6
Fu num=5

小贴士:Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu 类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。

9.5继承后的特点—成员方法

9.5.1成员方法不重名

如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。代码如下:

class Fu {
	public void show() {
		System.out.println("Fu类中的show方法执行");
	}
}
class Zi extends Fu {
	public void show2() {
		System.out.println("Zi类中的show2方法执行");
	}
}
public  class Demo05 {
	public static void main(String[] args) {
		Zi z = new Zi();
     	//子类中没有show方法,但是可以找到父类方法去执行
		z.show(); 
		z.show2();
	}
}

9.5.2 成员方法重名

如果子类父类中出现重名的成员方法,则创建子类对象调用该方法的时候,子类对象会优先调用自己的方法。

代码如下:

class Fu {
	public void show() {
		System.out.println("Fu show");
	}
}
class Zi extends Fu {
	//子类重写了父类的show方法
	public void show() {
		System.out.println("Zi show");
	}
}
public class ExtendsDemo05{
	public static void main(String[] args) {
		Zi z = new Zi();
     	// 子类中有show方法,只执行重写后的show方法
		z.show();  // Zi show
	}
}

9.6方法覆盖

回顾方法重载 overload

  1. 什么时候考虑使用方法重载? 在一个类中,如果功能相似,可以考虑使用方法重载。 这样做的目的是:代码美观,方便编程。
  2. 当满足什么条件的时候构成方法重载? 条件1:在同一个类中。 条件2:相同的方法名。 条件3:不同的参数列表:类型,个数,顺序
  3. 方法重载机制属于编译阶段的功能。(方法重载机制是给编译器看的。)

方法覆盖/ override/ 方法重写/ overwrite

  1. 什么时候考虑使用方法重写? 当从父类中继承过来的方法,无法满足子类的业务需求时。
  2. 当满足什么条件的时候,构成方法重写? 条件1:方法覆盖发生在具有继承关系的父子类之间。 条件2:具有相同的方法名(必须严格一样) 条件3:具有相同的形参列表(必须严格一样) 条件4:具有相同的返回值类型(可以是子类型) 3. 关于方法覆盖的细节:

3.1 当子类将父类方法覆盖之后,将来子类对象调用方法的时候,一定会执行重写之后的方法。 3.2 在java语言中,有一个注解,这个注解可以在编译阶段检查这个方法是否是重写了父类的方法。

Override注解是JDK5引入,用来标注方法,被标注的方法必须是重写父类的方法,如果不是重写的方法,编译器会报错。Override注解只在编译阶段有用,和运行期无关。

3.3 如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型
3.4 访问权限不能变低,可以变高。 3.5 抛出异常不能变多,可以变少。(后面学习异常的时候再说。)
3.6 私有的方法,以及构造方法不能继承,因此他们不存在方法覆盖。
3.7 方法覆盖针对的是实例方法。和静态方法无关。(讲完多态再说。)
3.8 方法覆盖针对的是实例方法。和实例变量没有关系。

// OverrideTest01.java

/**
 * 主类,用于演示方法覆盖(override)的示例。
 */
public class OverrideTest01 {
    public static void main(String[] args) {
        // 创建鸟儿对象
        Bird b = new Bird();

        // 调用方法
        b.eat(); // 调用的是从 Animal 类继承的 eat 方法
        b.move(); // 调用的是 Bird 类中覆盖了的 move 方法
    }
}
// Animal.java

/**
 * 动物类,包含吃和移动方法。
 */
public class Animal {
    /**
     * 动物吃的行为。
     */
    public void eat(){
        System.out.println("动物在吃东西");
    }

    /**
     * 动物移动的行为。
     */
    public void move(){
        System.out.println("动物在移动");
    }

    /**
     * 根据参数返回一个对象,用于演示方法覆盖中返回值类型的灵活性。
     *
     * @param a 参数a
     * @param b 参数b
     * @return 返回一个对象
     */
    public Object getObj(long a, String b){
        return null;
    }
}
// Bird.java

/**
 * 鸟类,继承自动物类,用于演示方法覆盖。
 */
public class Bird extends Animal{

    /**
     * Bird 对继承过来的 move() 方法不满意,因此对其进行了覆盖。
     */
    @Override
    public void move(){
        System.out.println("鸟儿在飞翔!");
    }

    /**
     * 从动物类继承过来的 getObj 方法被 Bird 类重写,返回值类型变为 String。
     *
     * @param a 参数a
     * @param b 参数b
     * @return 返回一个字符串
     */
    @Override
    public String getObj(long a, String b){
        return "";
    }

}

9.7继承后的特点—构造方法

当类之间产生了关系,其中各类中的构造方法,又产生了哪些影响呢? 首先我们要回忆两个事情,构造方法的定义格式和作用。

  1. 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
  2. 构造方法的作用是初始化对象成员变量数据的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。(先有爸爸,才能有儿子

继承后子类构方法器特点:子类所有构造方法的第一行都会默认先调用父类的无参构造方法

按如下需求定义类:

  1. 人类 成员变量: 姓名,年龄 成员方法: 吃饭
  2. 学生类 成员变量: 姓名,年龄,成绩 成员方法: 吃饭

代码如下:

class Person {
    private String name;
    private int age;

    public Person() {
        System.out.println("父类无参");
    }

    // getter/setter省略
}

class Student extends Person {
    private double score;

    public Student() {
        //super(); // 调用父类无参,默认就存在,可以不写,必须再第一行
        System.out.println("子类无参");
    }
    
     public Student(double score) {
        //super();  // 调用父类无参,默认就存在,可以不写,必须再第一行
        this.score = score;    
        System.out.println("子类有参");
     }

}

public class Demo07 {
    public static void main(String[] args) {
        Student s1 = new Student();
        System.out.println("----------");
        Student s2 = new Student(99.9);
    }
}

输出结果:
父类无参
子类无参
----------
父类无参
子类有参
  • 子类构造方法执行的时候,都会在第一行默认先调用父类无参数构造方法一次。
  • 子类构造方法的第一行都隐含了一个**super()**去调用父类无参数构造方法,**super()**可以省略不写。

10.多态

10.1向上转型和向下转型

关于基本数据类型之间的类型转换:

  1. 第一种:小容量转换成大容量,叫做自动类型转换。 int i = 100; long x = i;
  2. 第二种:大容量转换成小容量,不能自动转换,必须添加强制类型转换符才行。叫做强制类型转换。 int y = (int) x; 除了基本数据类型之间的类型转换之外,对于引用数据类型来说,也可以进行类型转换。 只不过不叫做自动类型转换和强制类型转换。我们一般称为向上转型和向下转型。 关于Java语言中的向上转型和向下转型:
  3. 向上转型(upcasting):子 ---> 父 (可以等同看做自动类型转换。)
  4. 向下转型(downcasting):父 ---> 子 (可以等同看做强制类型转换。) 不管是向上还是向下转型,两种类型之间必须要有继承关系,编译器才能编译通过。这是最基本的大前提。
public class TypeConversion {
    public static void main(String[] args) {
        // 自动类型转换:小容量转换成大容量
        int i = 100;
        long x = i; // 自动将int类型转换成long类型

        // 强制类型转换:大容量转换成小容量
        long y = 1000L;
        int z = (int) y; // 强制将long类型转换成int类型

        // 向上转型:子类对象转换成父类对象
        Animal animal = new Dog(); // 向上转型,Dog对象转换成Animal对象

        // 向下转型:父类对象转换成子类对象
        Animal animal2 = new Dog();
        Dog dog = (Dog) animal2; // 向下转型,Animal对象转换成Dog对象
    }
}

// 父类
class Animal {
    // 父类方法
}

// 子类
class Dog extends Animal {
    // 子类方法
}

在这个例子中,我们演示了基本数据类型之间的自动类型转换和强制类型转换,以及引用数据类型之间的向上转型和向下转型。请注意,在向下转型时,需要确保父类对象实际上是子类对象的实例,否则会出现运行时异常。

父类(Animal)                   子类(Dog)
  ↑                              ↑
  |                              |
向上转型                        向下转型
  |                              |
Animal animal = new Dog();   Dog dog = (Dog) animal;

向上转型(upcasting):

  1. 子 --> 父
  2. 也可以等同看做自动类型转换
  3. 前提:两种类型之间要有继承关系
  4. 父类型引用指向子类型对象。这个就是多态机制最核心的语法。

10.2多态的概念

java程序包括两个重要的阶段:

  1. 第一阶段:编译阶段
    在编译的时候,编译器只知道a2的类型是Animal类型。因此在编译的时候就会去Animal类中找move()方法。找到之后,绑定上去,此时发生静态绑定。能够绑定成功,表示编译通过。
  2. 第二阶段:运行阶段
    在运行的时候,堆内存中真实的java对象是Cat类型。所以move()的行为一定是Cat对象发生的。 因此运行的时候就会自动调用Cat对象的move()方法。这种绑定称为运行期绑定/动态绑定。

因为编译阶段是一种形态,运行的时候是另一种形态。因此得名:多态

package com.rainsoul.oop.oop19;

/**
 * Animal 类表示动物,定义了动物的一般行为。
 */
public class Animal {
    
    /**
     * 动物移动的方法。
     */
    public void move(){
        System.out.println("动物在移动");
    }

    /**
     * 动物进食的方法。
     */
    public void eat(){
        System.out.println("正在吃东西");
    }
}

package com.rainsoul.oop.oop19;

/**
 * Bird 类表示鸟类,继承自 Animal 类。
 */
public class Bird extends Animal{

    /**
     * 重写了动物移动的方法,表示鸟儿在飞翔。
     */
    @Override
    public void move() {
        System.out.println("鸟儿在飞翔");
    }

    /**
     * 鸟儿特有的歌唱方法。
     */
    public void sing(){
        System.out.println("鸟儿在歌唱!");
    }
}
package com.rainsoul.oop.oop19;

/**
 * Cat 类表示猫类,继承自 Animal 类。
 */
public class Cat extends Animal{

    /**
     * 重写了动物移动的方法,表示猫在走猫步。
     */
    @Override
    public void move() {
        System.out.println("猫在走猫步");
    }

    /**
     * 猫特有的抓老鼠方法。
     */
    public void catchMouse(){
        System.out.println("猫在抓老鼠");
    }
}
public class Test01 {
    public static void main(String[] args) {
        Animal a2 = new Cat();
        Animal a3 = new Bird();

        // 调用动物的移动方法,由于动态绑定,会执行对应子类的方法。
        a2.move();

        /*
         * 下面的代码是编译错误,因为编译器只知道a2是Animal类型,去Animal类中找
         * catchMouse()方法了,结果没有找到,无法完成静态绑定,编译报错。
         */
        //a2.catchMouse();

        /*
         * 假如现在就是要让a2去抓老鼠,怎么办?
         * 向下转型:downcasting(父--->子)
         * 什么时候我们会考虑使用向下转型?
         * 当调用的方法是子类中特有的方法。
         */
        Cat c2 = (Cat) a2;
        c2.catchMouse();

        // 多态
        Animal x = new Cat();

        // 向下转型,为了避免ClassCastException异常,使用instanceof运算符进行判断
        if (x instanceof Bird) {
            System.out.println("=========================");
            Bird y = (Bird) x;
        }

// instanceof 运算符的语法规则:
// 1. instanceof运算符的结果一定是:true/false
// 2. 语法格式:
//    (引用 instanceof 类型)
// 3. 例如:
//    (a instanceof Cat)
//      true表示什么?
//          a引用指向的对象是Cat类型。
//      false表示什么?
//          a引用指向的对象不是Cat类型。

        // 多态
        Animal a = new Bird();
        a.eat();

        // 需求:程序运行阶段动态确定对象
        // 如果对象是Cat,请抓老鼠。
        // 如果对象是Bird,请唱歌。
        if (a instanceof Cat) {
            Cat cat = (Cat) a;
            cat.catchMouse();
        } else if (a instanceof Bird) {
            Bird bird = (Bird) a;
            bird.sing();
        }
    }
}

10.3多态的优势

  1. 代码简洁: 多态能够通过统一的接口来处理不同类型的对象,从而简化了代码的编写。在代码中,不再需要为每个具体的对象编写特定的处理逻辑,而是通过多态机制实现统一的处理方式。

  2. 提高灵活性: 多态使得程序更加灵活,能够适应不同类型的变化。通过多态,可以在不改变代码结构的情况下,轻松地引入新的子类,扩展程序的功能。

  3. 提高可扩展性: 多态支持基于抽象类或接口编程,而不是具体类,从而降低了代码之间的耦合度。这使得程序更容易扩展和维护,因为可以通过扩展抽象类或接口来添加新的功能,而不会影响现有代码的稳定性。

  4. 提高代码复用性: 多态能够使得代码中的通用部分得到更多的复用。通过将相同的行为封装在抽象类或接口中,不同的子类可以共享这些行为,从而避免了重复编写相似的代码。

// 抽象动物类
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("狗啃骨头");
    }
}

// 主人类
class Master {
    // 喂食方法,利用多态
    public void feed(Animal animal) {
        animal.eat(); // 调用动物的吃方法,具体吃什么由实际的动物类决定
    }
}

// 测试类
public class TestPolymorphism {
    public static void main(String[] args) {
        // 创建主人对象
        Master master = new Master();
        // 创建猫对象和狗对象
        Animal cat = new Cat();
        Animal dog = new Dog();
        // 主人喂猫和狗,利用多态
        master.feed(cat); // 输出:猫吃鱼
        master.feed(dog); // 输出:狗啃骨头
    }
}

通过多态,主人类的feed方法可以接收不同类型的动物对象,并统一进行喂食操作。这样,在新增其他类型的动物时,不需要修改主人类的feed方法,实现了代码的灵活性和可扩展性。

10.4 多态的运行特点

调用成员变量时:编译看左边,运行看左边

调用成员方法时:编译看左边,运行看右边

代码示例:

Fu f = new Zi();
//编译看左边的父类中有没有name这个属性,没有就报错
//在实际运行的时候,把父类name属性的值打印出来
System.out.println(f.name);
//编译看左边的父类中有没有show这个方法,没有就报错
//在实际运行的时候,运行的是子类中的show方法
f.show();

方法覆盖针对的是实例方法。和静态方法无关。方法的覆盖和多态机制联合起来才有意义

这段代码展示了关于静态方法和实例变量的一些特性,让我们来总结一下:

  1. 静态方法的覆盖: 静态方法是属于类的方法,与对象无关,因此不存在方法覆盖的概念。在子类中定义与父类相同的静态方法,实际上是隐藏了父类的静态方法,并非方法的覆盖。

  2. 静态方法和多态: 多态是针对实例方法的,在多态中,父类引用可以指向子类对象。但对于静态方法,调用时直接通过类名来调用,不会受到多态的影响,因此无论引用指向哪个对象,调用的都是定义该静态方法的类的版本。

  3. 实例变量的覆盖: 实例变量是属于对象的属性,对于同名的实例变量,子类可以对其进行覆盖。但在实例方法中访问这个变量时,由于Java是根据引用的类型来绑定的,因此编译器会根据引用的类型选择变量,而不会根据对象的实际类型选择。

public class Animal {
    public static void test() {
        System.out.println("Animal's test method invoked");
    }
}

public class Cat extends Animal {
    public static void test() {
        System.out.println("Cat's test method invoked");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal.test();  // 输出:Animal's test method invoked
        Cat.test();     // 输出:Cat's test method invoked
        
        // 多态
        Animal a = new Cat();
        a.test();       // 输出:Animal's test method invoked
    }
}

public class Test2 {
    public static void main(String[] args) {
        // 多态
        A a = new B();
        // 输出:张三
        // 实例变量不存在覆盖这一说。  
		// a.name编译阶段绑定的是A类的name属性,运行的时候也会输出A类的name属性值。
        System.out.println(a.name);

        // 没有使用多态
        B b = new B();
        // 输出:李四
        System.out.println(b.name);
    }
}

class A {
    String name = "张三";
}

class B extends A {
    String name = "李四";
}

综上所述,静态方法在多态中调用时会绑定到定义该方法的类的版本,而实例变量的访问则是根据引用的类型进行绑定。

chatgpt解释:

[!chatgpt] 静态方法和实例变量之所以没有覆盖(即不具有方法覆盖和变量覆盖的特性),主要是因为它们的访问方式和绑定方式与实例方法和实例变量不同。下面分别解释一下:

  1. 静态方法:
    • 静态方法是属于类的方法,不是属于对象的方法。在调用静态方法时,不需要创建类的实例,而是直接通过类名来调用。因此,它们不受多态的影响,无论通过哪个子类的引用来调用,都会调用定义在父类中的静态方法。因此,静态方法不存在覆盖的概念,子类中定义与父类相同的静态方法实际上是隐藏了父类的静态方法。
  2. 实例变量:
    • 实例变量是属于对象的属性,它们的访问和绑定是根据对象的类型进行的。但是在Java中,子类中的实例变量不能完全覆盖父类中的实例变量,而是属于隐藏的关系。当子类中定义了与父类相同名称的实例变量时,会隐藏父类中的实例变量,但是在实际使用中,编译器会根据引用的类型来选择变量,而不会根据对象的实际类型来选择。

综上所述,静态方法和实例变量的访问方式和绑定方式不同于实例方法和实例变量,因此它们不具有方法覆盖和变量覆盖的特性。

10.5 总结

多态的基础语法主要涉及以下几个方面:

  1. 继承关系: 多态的实现基础是类与类之间的继承关系。在继承关系中,子类可以继承父类的属性和方法。

  2. 方法重写(覆盖): 子类可以重写(覆盖)父类的方法,以满足自身的业务需求。重写的方法必须与父类方法具有相同的签名(方法名、参数列表和返回类型)。

  3. 向上转型(upcasting): 向上转型是指将子类的实例赋值给父类类型的引用变量。这样的赋值操作是自动进行的,无需强制类型转换。通过向上转型,可以实现多态性,使得父类类型的引用变量可以指向子类对象。

  4. 向下转型(downcasting): 向下转型是指将父类类型的引用变量转换为子类类型的引用变量。这种转换需要显式地进行,并且在运行时可能会抛出ClassCastException异常。向下转型可以让我们调用子类特有的方法。

  5. instanceof运算符: instanceof运算符用于判断一个对象是否是某个类的实例。它的语法格式为(对象 instanceof 类型),返回结果为truefalse。通过使用instanceof运算符,可以在向下转型之前先判断对象的类型,避免发生类型转换异常。

  6. 动态绑定(运行期绑定): 在多态中,方法的调用是在运行时确定的,而不是在编译时确定的。这意味着,即使使用父类类型的引用变量来调用方法,实际执行的是子类对象的方法。这种绑定方式称为动态绑定或运行期绑定。

10.6 多态的弊端

我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类独有功能了

class Animal{
    public  void eat(){
        System.out.println("动物吃东西!")
    }
}
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
   
    public void catchMouse() {  
        System.out.println("抓老鼠");  
    }  
}  

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

class Test{
    public static void main(String[] args){
        Animal a = new Cat();
        a.eat();
        a.catchMouse();//编译报错,编译看左边,Animal没有这个方法
    }
}

11.super关键字

11.1基本使用方法

this.成员变量    	--    本类的
super.成员变量    	--    父类的

this.成员方法名()  	--    本类的    
super.成员方法名()   --    父类的
super(...) -- 调用父类的构造方法,根据参数匹配确认
this(...) -- 调用本类的其他构造方法,根据参数匹配确认
class Person {
    private String name ="凤姐";
    private int age = 20;

    public Person() {
        System.out.println("父类无参");
    }
    
    public Person(String name , int age){
        this.name = name ;
        this.age = age ;
    }

    // getter/setter省略
}

class Student extends Person {
    private double score = 100;

    public Student() {
        //super(); // 调用父类无参构造方法,默认就存在,可以不写,必须再第一行
        System.out.println("子类无参");
    }
    
     public Student(String name , int age,double score) {
        super(name ,age);// 调用父类有参构造方法Person(String name , int age)初始化name和age
        this.score = score;    
        System.out.println("子类有参");
     }
      // getter/setter省略
}

public class Demo07 {
    public static void main(String[] args) {
        // 调用子类有参数构造方法
        Student s2 = new Student("张三",20,99);
        System.out.println(s2.getScore()); // 99
        System.out.println(s2.getName()); // 输出 张三
        System.out.println(s2.getAge()); // 输出 20
    }
}

注意:

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。

super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

super(..)是根据参数去确定调用父类哪个构造方法的。

代码示例:

package com.rainsoul.oop.oop24;

/**
 * 父类 Person
 */
public class Person {
    String name;
    int age;
    String email;
    String address;

    /**
     * 无参构造方法
     */
    public Person() {
        super(); // 调用父类的无参构造方法
    }

    // 省略了getter和setter方法

    /**
     * 实例方法,用于描述人类正在做一些事情
     */
    public void doSome(){
        System.out.println("人类正在做一些事情!");
    }
}

package com.rainsoul.oop.oop24;

/**
 * 子类 Teacher
 */
public class Teacher extends Person {
    double sal; // 特有的属性:工资
    String name; // 子类中与父类同名的属性

    public Teacher() {
    }

    public Teacher(String name, int age, String email, String address, double sal) {
        // 调用父类的属性初始化方法
        super.name = name; // 通过super调用父类的属性
        this.name = name; // 子类中的同名属性
        this.age = age;
        this.email = email;
        this.address = address;
        this.sal = sal;
    }

    // 省略了getter和setter方法

    /**
     * 显示教师的信息
     */
    public void display() {
        // 输出父类的属性值
        System.out.println("姓名:" + super.name);
        System.out.println("年龄:" + super.age);
        System.out.println("邮箱:" + super.email);
        System.out.println("住址:" + super.address);
        System.out.println("工资:" + this.sal);

        // 输出子类的同名属性值
        System.out.println("姓名:" + this.name);
        System.out.println("年龄:" + this.age);
        System.out.println("邮箱:" + this.email);
        System.out.println("住址:" + this.address);
        System.out.println("工资:" + this.sal);
    }

    /**
     * 重写父类的实例方法
     */
    @Override
    public void doSome() {
        System.out.println("do some开始执行了");
        super.doSome(); // 调用父类的doSome方法
        System.out.println("do some方法执行结束了");
        // 输出this和super的区别
        System.out.println(this); // 输出当前对象
        //super本身不是一个引用。super只是代表了当前对象的父类型特征那部分。
        // super 不能够单独的输出。  
		//System.out.println(super); // 编译报错。
    }
}
package com.rainsoul.oop.oop24;

/**
 * 测试类 Test01
 */
public class Test01 {
    public static void main(String[] args) {
        // 创建Teacher对象
        Teacher t = new Teacher("张三", 20, "[email protected]", "北京朝阳", 10000.0);
        // 显示教师的信息
        t.display();
        // 调用doSome方法
        t.doSome();
    }
}

11.2super用法总结

在Java中,super关键字用于引用父类的成员,包括属性、方法和构造方法。它主要有以下几种使用方法:

  1. 访问父类的属性或方法: 可以使用super关键字来访问父类中被子类覆盖的成员或者在子类中隐藏的成员。
super.name; // 访问父类的属性
super.doSome(); // 调用父类的实例方法
  1. 调用父类的构造方法: 在子类的构造方法中通过super()调用父类的构造方法。通常用于子类构造方法的第一行。
public Teacher(String name, int age, String email, String address, double sal) {
    super(name, age, email, address); // 调用父类的构造方法
    this.sal = sal;
}
  1. 在子类中调用父类的构造方法后,可以继续进行子类属性的初始化:
super.name = name; // 通过super调用父类的属性
this.name = name; // 子类中的同名属性
  1. 在重写父类方法时,通过super关键字可以显式调用父类的被重写方法:
@Override
public void doSome() {
    super.doSome(); // 调用父类的doSome方法
}

除了上述提到的使用方法之外,super关键字还可以用于以下情况:

  1. 在内部类中使用外部类的成员变量: 当内部类和外部类有同名属性时,为了区分,可以使用外部类.super来引用外部类的成员变量。
public class Outer {
    private int x = 10;

    class Inner {
        private int x = 20;

        public void test() {
            System.out.println(x);          // 输出内部类的x,结果为20
            System.out.println(this.x);     // 输出内部类的x,结果为20
            System.out.println(Outer.this.x); // 使用外部类的x,结果为10
        }
    }
}
  1. 在接口的默认方法中调用父接口的默认方法: 当一个接口继承了另一个接口时,可以使用super关键字来调用父接口的默认方法。
public interface InterfaceA {
    default void method() {
        System.out.println("InterfaceA's default method");
    }
}

public interface InterfaceB extends InterfaceA {
    default void method() {
        InterfaceA.super.method(); // 调用父接口InterfaceA的默认方法
        System.out.println("InterfaceB's default method");
    }
}

11.3静态上下文无法使用this和super

在Java中,静态上下文是指在静态方法或静态代码块中。在这些地方,不能使用thissuper关键字,因为它们没有明确的对象实例来引用。

public class MyClass {
    private static int staticVar = 10;
    private int instanceVar = 20;

    public static void staticMethod() {
        // 在静态方法中无法使用this关键字
        // System.out.println(this.instanceVar); // 编译错误
        // System.out.println(super.staticVar); // 编译错误
    }

    public void instanceMethod() {
        // 在实例方法中可以使用this关键字来引用当前对象的实例变量
        System.out.println(this.instanceVar); // 输出20
        // 在实例方法中无法使用super关键字,因为super关键字用于引用父类的成员,而不是当前对象
        // System.out.println(super.staticVar); // 编译错误
    }

    public static void main(String[] args) {
        // 在静态方法中无法直接调用实例方法,因为静态方法是属于类的,没有隐含的对象实例
        // instanceMethod(); // 编译错误

        // 在静态方法中可以调用静态变量
        System.out.println(staticVar); // 输出10

        // 在静态方法中无法使用this关键字
        // System.out.println(this.instanceVar); // 编译错误
    }
}

在静态方法中,不能使用this关键字引用当前对象的实例变量,也不能使用super关键字引用父类的成员,因为静态方法是与类关联的,而不是对象。

总结:

  • 子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。

  • super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

  • super(..)和this(...)是根据参数去确定调用父类哪个构造方法的。

  • super(..)可以调用父类构造方法初始化继承自父类的成员变量的数据。

  • this(..)可以调用本类中的其他构造方法。

12.final关键字

12.1 概述

学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。如果有一个方法我不想别人去改写里面内容,该怎么办呢? Java提供了final 关键字,表示修饰的内容不可变。

  • final
  • 不可改变,最终的含义。可以用于修饰类、方法和变量。
  • 类:被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,有且仅能被赋值一次。

12.2 使用方式

12.2.1 修饰类

final修饰的类,不能被继承。 格式如下:

final class 类名 {  
}

代码:

final class Fu {  
}  
// class Zi extends Fu {} // 报错,不能继承final的类

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多我们学习过的类,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。

12.2.2 修饰方法

final修饰的方法,不能被重写。 格式如下:

修饰符 final 返回值类型 方法名(参数列表){  
    //方法体  
}

代码:

class Fu2 {  
    final public void show1() {  
        System.out.println("Fu2 show1");  
    }  
    public void show2() {  
        System.out.println("Fu2 show2");  
    }  
}  
​  
class Zi2 extends Fu2 {  
//  @Override  
//  public void show1() {  
//  System.out.println("Zi2 show1");  
//  }  
    @Override  
    public void show2() {  
        System.out.println("Zi2 show2");  
    }  
}

12.2.3 修饰变量-局部变量

  1. 局部变量——基本类型 基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。代码如下:
public class FinalDemo1 {  
    public static void main(String[] args) {  
        // 声明变量,使用final修饰  
        final int a;  
        // 第一次赋值   
        a = 10;  
        // 第二次赋值  
        a = 20; // 报错,不可重新赋值  
​  
        // 声明变量,直接赋值,使用final修饰  
        final int b = 10;  
        // 第二次赋值  
        b = 20; // 报错,不可重新赋值  
    }  
}

思考,下面两种写法,哪种可以通过编译?

写法1:

final int c = 0;
for (int i = 0; i < 10; i++) {
    c = i;
    System.out.println(c);
}

写法2:

for (int i = 0; i < 10; i++) {  
    final int c = i;  
    System.out.println(c);  
}

根据 final 的定义,写法1报错!写法2为什么通过编译呢?因为每次循环,都是一次新的变量c。

12.2.4 修饰变量-成员变量

成员变量涉及到初始化的问题,初始化方式有显示初始化和构造方法初始化,只能选择其中一个:

  • 显示初始化(在定义成员变量的时候立马赋值)(常用);
public class Student {  
    final int num = 10;  
}
  • 构造方法初始化(在构造方法中赋值一次)(不常用,了解即可)。 注意:每个构造方法中都要赋值一次!
public class Student {
    final int num = 10;
    final int num2;

    public Student() {
        this.num2 = 20;
//     this.num2 = 20;
    }
    
     public Student(String name) {
        this.num2 = 20;
//     this.num2 = 20;
    }
}

被final修饰的常量名称,一般都有书写规范,所有字母都大写

13.权限修饰符

13.1 权限修饰符

在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,我们之前已经学习过了public 和 private,接下来我们研究一下protected和默认修饰符的作用。

  • public:公共的,所有地方都可以访问。
  • protected:本类 ,本包,其他包中的子类都可以访问。
  • 默认(没有修饰符):本类 ,本包可以访问。 注意:默认是空着不写,不是default
  • private:私有的,当前类可以访问。 public > protected > 默认 > private

3.2 不同权限的访问能力

public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用private ,隐藏细节。
  • 构造方法使用public ,方便创建对象。
  • 成员方法使用public ,方便调用方法。

小贴士:不加权限修饰符,就是默认权限

14抽象类和抽象方法

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类

  • 抽象方法 : 没有方法体的方法。
  • 抽象类:包含抽象方法的类。

14.1abstract使用格式

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

14.1.1抽象方法

使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

修饰符 abstract 返回值类型 方法名 (参数列表);

14.1.2 抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。

定义格式:

abstract class 类名字 { 
  
}

14.2 抽象类的使用

要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。

代码举例:

// 父类,抽象类
abstract class Employee {
	private String id;
	private String name;
	private double salary;
	
	public Employee() {
	}
	
	public Employee(String id, String name, double salary) {
		this.id = id;
		this.name = name;
		this.salary = salary;
	}
	
	// 抽象方法
	// 抽象方法必须要放在抽象类中
	abstract public void work();
}

class Manager extends Employee {
	public Manager() {
	}
	public Manager(String id, String name, double salary) {
		super(id, name, salary);
	}
	// 2.重写父类的抽象方法
	@Override
	public void work() {
		System.out.println("管理其他人");
	}
}

class Cook extends Employee {
	public Cook() {
	}
	public Cook(String id, String name, double salary) {
		super(id, name, salary);
	}
	@Override
	public void work() {
		System.out.println("厨师炒菜多加点盐...");
	}
}
// 测试类
public class Demo10 {
	public static void main(String[] args) {
//创建抽象类,抽象类不能创建对象
//假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象
//		Employee e = new Employee();
//		e.work();
		
		// 3.创建子类
		Manager m = new Manager();
		m.work();
		
		Cook c = new Cook("ap002", "库克", 1);
		c.work();
	}
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

14.3抽象类的特征

抽象类的特征总结起来可以说是 有得有失 有得:抽象类得到了拥有抽象方法的能力。 有失:抽象类失去了创建对象的能力。

其他成员(构造方法,实例方法,静态方法等)抽象类都是具备的。

14.4抽象类的细节

不需要背,只要当idea报错之后,知道如何修改即可。 关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  1. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  1. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  1. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。

理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

  1. 抽象类存在的意义是为了被子类继承。

理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

15.5 抽象类存在的意义

抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。

17.接口

我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7,接口中全部是抽象方法。接口同样是不能创建对象的

17.1定义格式

//接口的定义格式:
interface 接口名称{
    // 抽象方法
}

// 接口的声明:interface
// 接口名称:首字母大写,满足“驼峰模式”

17.2接口成分的特点

在JDK7,包括JDK7之前,接口中的只有包含:抽象方法和常量

17.2.1抽象方法

注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!! ​ 按照规范:以后接口中的抽象方法建议不要写上public abstract。

17.2.2常量

在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。

public interface InterF {
    // 抽象方法!
    //    public abstract void run();
    void run();

    //    public abstract String getName();
    String getName();

    //    public abstract int add(int a , int b);
    int add(int a , int b);


    // 它的最终写法是:
    // public static final int AGE = 12 ;
    int AGE  = 12; //常量
    String SCHOOL_NAME = "北京邮电大学";

}

17.3基本的实现

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

17.3.1实现接口的格式

/**接口的实现:
    在Java中接口是被实现的,实现接口的类称为实现类。
    实现类的格式:*/
class 类名 implements 接口1,接口2,接口3...{

}

从上面格式可以看出,接口是可以被多实现的。

17.3.2类实现接口的要求和意义

  1. 必须重写实现的全部接口中所有抽象方法。
  2. 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。
  3. 意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。
/**
   接口:接口体现的是规范。
 * */
public interface SportMan {
    void run(); // 抽象方法,跑步。
    void law(); // 抽象方法,遵守法律。
    String compittion(String project);  // 抽象方法,比赛。
}

接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下:

/**
 * 接口的实现:
 *    在Java中接口是被实现的,实现接口的类称为实现类。
 *    实现类的格式:
 *      class 类名 implements 接口1,接口2,接口3...{
 *
 *
 *      }
 * */
public class PingPongMan  implements SportMan {
    @Override
    public void run() {
        System.out.println("乒乓球运动员稍微跑一下!!");
    }

    @Override
    public void law() {
        System.out.println("乒乓球运动员守法!");
    }

    @Override
    public String compittion(String project) {
        return "参加"+project+"得金牌!";
    }
}
public class TestMain {
    public static void main(String[] args) {
        // 创建实现类对象。
        PingPongMan zjk = new PingPongMan();
        zjk.run();
        zjk.law();
        System.out.println(zjk.compittion("全球乒乓球比赛"));

    }
}

类与接口的多实现案例:

/** 法律规范:接口*/
public interface Law {
    void rule();
}

/** 这一个运动员的规范:接口*/
public interface SportMan {
    void run();
}

/**
 * Java中接口是可以被多实现的:
 *    一个类可以实现多个接口: Law, SportMan
 *
 * */
public class JumpMan implements Law ,SportMan {
    @Override
    public void rule() {
        System.out.println("尊长守法");
    }

    @Override
    public void run() {
        System.out.println("训练跑步!");
    }
}

从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。

17.4接口与接口的多继承

Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。

类与接口是实现关系

接口与接口是继承关系

接口继承接口就是把其他接口的抽象方法与本接口进行了合并。

案例演示:

public interface Abc {
    void go();
    void test();
}

/** 法律规范:接口*/
public interface Law {
    void rule();
    void test();
}

 *
 *  总结:
 *     接口与类之间是多实现的。
 *     接口与接口之间是多继承的。
 * */
public interface SportMan extends Law , Abc {
    void run();
}

17.5扩展:接口的细节

不需要背,只要当idea报错之后,知道如何修改即可。 关于接口的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 当两个接口中存在相同抽象方法的时候,该怎么办?

只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。

  1. 实现类能不能继承A类的时候,同时实现其他接口呢?

继承的父类,就好比是亲爸爸一样 实现的接口,就好比是干爹一样 可以继承一个类的同时,再实现多个接口,只不过,要把接口里面所有的抽象方法,全部实现。

  1. 实现类能不能继承一个抽象类的时候,同时实现其他接口呢?

实现类可以继承一个抽象类的同时,再实现其他多个接口,只不过要把里面所有的抽象方法全部重写。

  1. 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?

处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。 处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。

  1. 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?

可以在接口跟实现类中间,新建一个中间类(适配器类) 让这个适配器类去实现接口,对接口里面的所有的方法做空重写。 让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。 因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象

17.6总结

  1. 接口(interface)在Java中表示一种规范或契约,它定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性。 接口和类一样,也是一种引用数据类型
  2. 接口怎么定义? [修饰符列表] interface 接口名{}
  3. 抽象类是半抽象的,接口是完全抽象的。接口没有构造方法,也无法实例化
  4. 4.(JDK8之前的语法规则) 接口中只能定义:常量+抽象方法。接口中的常量的static final可以省略。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的
  5. 接口和接口之间可以多继承
  6. 类和接口的关系我们叫做实现(这里的实现也可以等同看做继承)。使用implements关键字进行接口的实现。
  7. 一个非抽象的类实现接口必须将接口中所有的抽象方法全部实现(强制要求的,必须的,要不然编译器报错。)
  8. 一个类可以实现多个接口。语法是:class 类 implements 接口A,接口B{}
  9. Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性) 默认方法:引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。所有实现接口的类都必须实现这些抽象方法。这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时, 这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。这就是所谓的"接口演变"问题 静态方法:注意:java中规定,在JDK8之后,接口中可以一定静态方法,但是这个静态方法,只能通过“该接口名”去调用的。别的都无法调用。 在JDK8之后引入接口可以定义静态方法,实际上想表达一个意思:接口也可以作为工具来使用了。
  10. JDK9之后允许接口中定义私有的实例方法(为默认方法服务的)和私有的静态方法(为静态方法服务的)
  11. 所有的接口隐式的继承Object。因此接口也可以调用Object类的相关方法

18.内部类

18.1 概述

18.1.1 什么是内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。可以把内部类理解成寄生,外部类理解成宿主。

18.1.2 什么时候使用内部类

一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用

  1. 人里面有一颗心脏。
  2. 汽车内部有一个发动机。
  3. 为了实现更好的封装性。

18.2 内部类的分类

按定义的位置来分

  1. 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
  2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
  3. 局部内部类,类定义在方法内
  4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

18.3 成员内部类

成员内部类特点

  • 无static修饰的内部类,属于外部类对象的。
  • 宿主:外部类对象。

内部类的使用格式

外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类

获取成员内部类对象的两种方式

方式一:外部直接创建成员内部类的对象

外部类.内部类 变量 = new 外部类().new 内部类();

方式二:在外部类中定义一个方法提供内部类的对象

案例演示

方式一:

public class Test {  
    public static void main(String[] args) {  
        //  宿主:外部类对象。  
       // Outer out = new Outer();  
        // 创建内部类对象。  
        Outer.Inner oi = new Outer().new Inner();  
        oi.method();  
    }  
}  
​  
class Outer {  
    // 成员内部类,属于外部类对象的。  
    // 拓展:成员内部类不能定义静态成员。  
    public class Inner{  
        // 这里面的东西与类是完全一样的。  
        public void method(){  
            System.out.println("内部类中的方法被调用了");  
        }  
    }  
}  



方式二:

public class Outer {  
    String name;  
    private class Inner{  
        static int a = 10;  
    }  
    public Inner getInstance(){  
        return new Inner();  
    }  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        Outer o = new Outer();  
        System.out.println(o.getInstance());  
​  
​  
    }  
}

18.4 成员内部类的细节

编写成员内部类的注意点:

  1. 成员内部类可以被一些修饰符所修饰,比如: private,默认,protected,public,static等
  2. 在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。
  3. 创建内部类对象时,对象中有一个隐含的Outer.this记录外部类对象的地址值。

18.5 成员内部类面试题

请在?地方向上相应代码,以达到输出的内容

注意:内部类访问外部类对象的格式是:外部类名.this

public class Test {  
    public static void main(String[] args) {  
        Outer.inner oi = new Outer().new inner();  
        oi.method();  
    }  
}  
​  
class Outer {   // 外部类  
    private int a = 30;  
​  
    // 在成员位置定义一个类  
    class inner {  
        private int a = 20;  
​  
        public void method() {  
            int a = 10;  
            System.out.println(???);    // 10   答案:a  
            System.out.println(???);    // 20   答案:this.a  
            System.out.println(???);    // 30   答案:Outer.this.a  
        }  
    }  
}

18.6 成员内部类内存图

内部类内存图转存失败,建议直接上传图片文件

18.7 静态内部类

静态内部类特点

  • 静态内部类是一种特殊的成员内部类。
  • 有static修饰,属于外部类本身的。
  • 总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。
  • 拓展1:静态内部类可以直接访问外部类的静态成员。
  • 拓展2:静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。
  • 拓展3:静态内部类中没有银行的Outer.this。

内部类的使用格式

外部类.内部类。

静态内部类对象的创建格式

外部类.内部类  变量 = new  外部类.内部类构造器;

调用方法的格式:

  • 调用非静态方法的格式:先创建对象,用对象调用
  • 调用静态方法的格式:外部类名.内部类名.方法名(); 案例演示
// 外部类:Outer01  
class Outer01{  
    private static  String sc_name = "程序员";  
    // 内部类: Inner01  
    public static class Inner01{  
        // 这里面的东西与类是完全一样的。  
        private String name;  
        public Inner01(String name) {  
            this.name = name;  
        }  
        public void showName(){  
            System.out.println(this.name);  
            // 拓展:静态内部类可以直接访问外部类的静态成员。  
            System.out.println(sc_name);  
        }  
    }  
}  
​  
public class InnerClassDemo01 {  
    public static void main(String[] args) {  
        // 创建静态内部类对象。  
        // 外部类.内部类  变量 = new  外部类.内部类构造器;  
        Outer01.Inner01 in  = new Outer01.Inner01("张三");  
        in.showName();  
    }  
}

18.8 局部内部类

  • 局部内部类 :定义在方法中的类。

定义格式:

class 外部类名 {  
    数据类型 变量名;  
      
    修饰符 返回值类型 方法名(参数列表) {  
        // …  
        class 内部类 {  
            // 成员变量  
            // 成员方法  
        }  
    }  
}

18.9 匿名内部类【重点】

18.9.1 概述

匿名内部类 :是内部类的简化写法。他是一个隐含了名字的内部类。开发中,最常用到的内部类就是匿名内部类了。

18.9.2 格式

new 类名或者接口名() {  
     重写方法;  
};

包含了:

  • 继承或者实现关系
  • 方法重写
  • 创建对象

所以从语法上来讲,这个整体其实是匿名内部类对象

18.9.3 什么时候用到匿名内部类

实际上,如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用

是为了简化代码

之前我们使用接口时,似乎得做如下几步操作:

  1. 定义子类
  2. 重写接口中的方法
  3. 创建子类对象
  4. 调用重写后的方法
interface Swim {  
    public abstract void swimming();  
}  
​  
// 1. 定义接口的实现类  
class Student implements Swim {  
    // 2. 重写抽象方法  
    @Override  
    public void swimming() {  
        System.out.println("狗刨式...");  
    }  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        // 3. 创建实现类对象  
        Student s = new Student();  
        // 4. 调用方法  
        s.swimming();  
    }  
}

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。

18.9.4 匿名内部类前提和格式

匿名内部类必须继承一个父类或者实现一个父接口

匿名内部类格式

new 父类名或者接口名(){  
    // 方法重写  
    @Override   
    public void method() {  
        // 执行语句  
    }  
};

18.9.5 使用方式

以接口为例,匿名内部类的使用,代码如下:

interface Swim {  
    public abstract void swimming();  
}  
​  
public class Demo07 {  
    public static void main(String[] args) {  
        // 使用匿名内部类  
        new Swim() {  
            @Override  
            public void swimming() {  
                System.out.println("自由泳...");  
            }  
        }.swimming();  
​  
        // 接口 变量 = new 实现类(); // 多态,走子类的重写方法  
        Swim s2 = new Swim() {  
            @Override  
            public void swimming() {  
                System.out.println("蛙泳...");  
            }  
        };  
​  
        s2.swimming();  
        s2.swimming();  
    }  
}

18.9.6 匿名内部类的特点

  1. 定义一个没有名字的内部类
  2. 这个类实现了父类,或者父类接口
  3. 匿名内部类会创建这个没有名字的类的对象

18.9.7 匿名内部类的使用场景

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:

interface Swim {  
    public abstract void swimming();  
}  
​  
public class Demo07 {  
    public static void main(String[] args) {  
        // 普通方式传入对象  
        // 创建实现类对象  
        Student s = new Student();  
          
        goSwimming(s);  
        // 匿名内部类使用场景:作为方法参数传递  
        Swim s3 = new Swim() {  
            @Override  
            public void swimming() {  
                System.out.println("蝶泳...");  
            }  
        };  
        // 传入匿名内部类  
        goSwimming(s3);  
​  
        // 完美方案: 一步到位  
        goSwimming(new Swim() {  
            public void swimming() {  
                System.out.println("大学生, 蛙泳...");  
            }  
        });  
​  
        goSwimming(new Swim() {  
            public void swimming() {  
                System.out.println("小学生, 自由泳...");  
            }  
        });  
    }  
​  
    // 定义一个方法,模拟请一些人去游泳  
    public static void goSwimming(Swim s) {  
        s.swimming();  
    }  
}

19.Object类中的方法

19.1Object类中的toString()方法

在Java中,所有的类都直接或间接地继承自Object类。Object类中的toString()方法是一个非常重要的方法,它用于返回对象的字符串表示形式。

  1. 目的

    • toString()方法的目的是将对象的内容转换为字符串形式,通常用于将对象的信息转换为可读性更好的格式,便于打印、日志记录和调试等操作。
    • 这个方法在调试过程中非常有用,因为它可以提供有关对象状态的快速摘要,而无需深入检查对象的每个属性。
  2. 默认实现

    • Object类中,toString()方法的默认实现返回一个字符串,其中包含对象的类名,以及对象在内存中的哈希码的十六进制表示。该默认实现的方法签名如下:
      public String toString() {
          return getClass().getName() + "@" + Integer.toHexString(hashCode());
      }
      
    • 例如,对于一个类MyClass的实例,如果没有在MyClass中重写toString()方法,那么调用toString()方法将返回类似于MyClass@abcd1234的字符串,其中abcd1234是对象的哈希码的十六进制表示。可以等同看做地址。
  3. 重写

    • 在实际开发中,通常需要根据对象的实际内容来自定义toString()方法,以便更清晰地表示对象的状态。
    • 为了重写toString()方法,只需在自定义类中提供一个与toString()方法签名相同的方法,并且在其中返回所需的字符串表示形式即可。

下面是一个示例,展示了如何在自定义类中重写toString()方法:

public class Person {
    private String name;
    private int age;

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

    // 重写toString()方法
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }

    public static void main(String[] args) {
        Person person = new Person("John", 30);
        System.out.println(person.toString()); // 调用toString()方法
    }
}

在上面的示例中,Person类重写了toString()方法,以返回包含对象姓名和年龄的字符串表示形式。当调用toString()方法时,它返回类似于Person{name='John', age=30}的字符串。

println方法源码分析:

image.png

当println()输出的是一个引用的时候,会自动调用“引用.toString()”。但是自己手动调用容易出现空指针异常。

19.2Object类中的equals()方法

你已经提供了一个很好的总结和解释关于Object类中的equals方法。让我简要概括一下:

  1. 目的equals方法的设计目的是用于判断两个对象是否相等。这里的"相等"通常意味着两个对象在逻辑上具有相同的内容或状态。

  2. 默认实现:在Object类中,equals方法的默认实现是使用==运算符比较两个对象的引用,即比较它们在内存中的地址是否相同。默认实现如下:

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

    这意味着只有当两个对象引用指向相同的内存地址时,equals方法返回true,否则返回false

  3. 关于 == 运算符的运算规则== 运算符用于比较变量中保存的值。如果这些值是基本数据类型,则比较它们的实际值;如果这些值是对象的引用,则比较的是它们在内存中的地址。

  4. 重写原因:由于Object类中的equals方法比较的是对象的引用,而不是对象的内容,因此在许多情况下,我们希望自定义类的对象根据其内容是否相同来判断是否相等。因此,我们需要重写equals方法,以便根据对象的内容来进行比较。通常情况下,我们会重写equals方法来比较对象的字段,以确保只要这些字段相等,就认为对象相等。

19.3Object类的hashCode() 方法

你已经总结得很详细了,让我稍作补充和概括:

  1. 作用hashCode()方法返回一个对象的哈希码,通常用作在哈希表等数据结构中查找对象的键值,以提高查找效率。

  2. 默认实现:在Object类中,hashCode()方法的默认实现是通过将对象的内存地址转换为整数作为哈希值。这意味着如果两个对象的引用不同,它们的哈希码也会不同。默认实现如下:

    public native int hashCode();
    

    这是一个本地方法,底层实现是使用C++编写的动态链接库程序。

  3. 优化集合类hashCode()方法的存在是为了优化像HashMapHashtableHashSet等集合类的性能。通过使用哈希码,这些集合类可以更快地确定对象在集合中的位置,从而实现快速的查找和存储。

  4. 重要性:虽然hashCode()方法的默认实现通常足够满足需求,但在某些情况下,可能需要根据对象的内容来重新实现hashCode()方法,以确保相等的对象具有相同的哈希码。这对于需要自定义类的对象在集合中正常工作的情况特别重要。

19.4Object类的finalize()方法

  1. 作用finalize()方法用于在Java对象被垃圾回收器(GC)回收之前执行一些清理操作。通常,这个方法被用来释放对象所持有的系统资源或执行其他清理操作,以便对象在被销毁前完成必要的准备工作。

  2. Java 9 中的过时标记:从Java 9开始,finalize()方法被标记为过时(Deprecated),意味着不建议再使用这个方法。这是因为finalize()方法有一些缺点和不确定性,包括不能保证及时执行、性能开销大等问题,而且有更好的替代方法来管理资源,如使用try-with-resources语句或手动关闭资源。

  3. 默认实现:在Object类中,finalize()方法的默认实现是空方法,即什么也不做。因此,如果一个类需要在对象被回收前执行特定的清理操作,通常需要在子类中重写finalize()方法,并在其中实现相应的逻辑。

  4. 重写规则:当子类重写finalize()方法时,通常应该在方法体中完成资源的释放或其他清理操作,并且在方法的末尾调用super.finalize()以确保调用父类的finalize()方法。

19.5Object类的clone()方法

  1. 作用clone()方法用于创建并返回一个对象的副本,通常被用于需要保护原始对象数据结构的情况下。通过克隆,可以生成一个新的对象,对其进行操作而不影响原始对象。

  2. 默认实现:在Object类中,clone()方法的默认实现是一个受保护的本地方法,专门给子类使用。默认实现如下:

    protected native Object clone() throws CloneNotSupportedException;
    

    这意味着默认的clone()方法只能在Object的子类中使用,且需要处理CloneNotSupportedException异常。

  3. 调用方法:为了调用clone()方法,需要在子类中重写该方法,并将其访问修饰符修改为public。这样可以确保在任何位置都可以调用clone()方法。子类重写clone()方法时,应当调用super.clone()以获得父类的克隆对象,并进行进一步的处理。

  4. Cloneable接口:为了参与克隆,类必须实现Cloneable接口,这是一个标志性接口,没有定义任何方法。在Java中,有两种类型的接口:标志性接口和普通接口。Cloneable属于标志性接口,用于表示类支持克隆操作。

总之,clone()方法提供了对象的复制功能,但使用时需要注意实现Cloneable接口并正确重写clone()方法。通常情况下,建议在重写clone()方法时使用super.clone()来获得父类的克隆对象,然后再进行进一步的处理。