Lesson10 Object类、最终类、内部类、匿名类

1 Object类

  • 基于多态的特性,该类可以用来代表任何一个类,因此允许把任何类型的对象赋给 Object类型的变量,也可以作为方法的参数、方法的返回值

Object类提供的几个关键方法

Pasted image 20241125161632.png

public final Class<?> getClass(){}
  • 该方法用于获取对象运行时的字节码类型,得到该对象的运行时的真实类型
  • 通常用于判断两个引用中实际存储对象类型是否一致。
public class MyClass {
    // 类的成员变量和方法
}

public class AnotherClass {
    // 另一个类的成员变量和方法
}

public class TypeComparisonExample {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass();
        MyClass obj2 = new MyClass();
        AnotherClass obj3 = new AnotherClass();

        // 比较obj1和obj2是否指向相同类型的对象
        if (obj1.getClass() == obj2.getClass()) {
            System.out.println("obj1和obj2指向相同类型的对象");
        } else {
            System.out.println("obj1和obj2指向不同类型的对象");
        }

        // 比较obj1和obj3是否指向相同类型的对象
        if (obj1.getClass() == obj3.getClass()) {
            System.out.println("obj1和obj3指向相同类型的对象");
        } else {
            System.out.println("obj1和obj3指向不同类型的对象");
        }
    }
}
obj1和obj2指向相同类型的对象
obj1和obj3指向不同类型的对象
  • 最主要应用:该方法属于Java的反射机制,其返回值是Class类型,例如 Class c = obj.getClass();。通过对象c,我们可以进一步获取该对象的所有成员方法,每个成员方法都是一个Method对象。我们也可以获取该对象的所有成员变量,每个成员变量都是一个Field对象。同样的,我们也可以获取该对象的构造函数,构造函数则是一个Constructor对象
// 用getClass来获得该对象的类名、所有成员方法、所有成员变量、所有构造函数
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class MyClass {
    private int myField;
    public MyClass() {
        // 默认构造函数
    }
    public MyClass(int value) {
        // 带参数的构造函数
        myField = value;
    }
    public void myMethod() {
        // 成员方法
    }
}

public class ReflectionExample {
    public static void main(String[] args) {
        MyClass obj = new MyClass();

        // 获取Class对象
        Class<?> cls = obj.getClass();

        // 获取类名
        String className = cls.getName();
        System.out.println("类名: " + className);

        // 获取所有成员方法
        Method[] methods = cls.getDeclaredMethods();
        System.out.println("成员方法:");
        for (Method method : methods) {
            System.out.println(method.getName());
        }

        // 获取所有成员变量
        Field[] fields = cls.getDeclaredFields();
        System.out.println("成员变量:");
        for (Field field : fields) {
            System.out.println(field.getName());
        }

        // 获取所有构造函数
        Constructor<?>[] constructors = cls.getDeclaredConstructors();
        System.out.println("构造函数:");
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor.getName());
        }
    }
}
public int hashCode(){}
  • 返回该对象的哈希码值。哈希值为根据对象的地址或字符串或数字使用hash算法计算出来的int类型的数值。
  • 在Object类中,hashCode的默认实现通常会返回对象的内存地址的某种形式(不能完全将哈希值等价于地址),具体的实现依赖JVM。
  • 提高具有哈希结构的容器的效率。
  • 如果两个对象相等(即equals返回true),那么它们的hashCode值也必须相等。
public boolean equals(Object obj)
  • 比较两个对象是否相等。仅当被比较的两个引用变量指向同一对象时(即两个对象地址相同,也即hashCode值相同),equals()方法返回true
  • 可进行覆盖,比较两个对象的内容是否相同。
equals()hashCode

equals为true与hashCode相同的关系?

  • 如果两个对象的equals()结果为true,那么这两个对象的hashCode()一定相同
  • 两个对象的hashCode()结果相同,并不能代表两个对象的equals()一定为true(Hash散列值有冲突的情况,虽然概率很低,只能够说明这两个对象在一个散列存储结构中)

为什么要重写hashCode和equals?

  • equals() ⽅法⽤于⽐较两个对象的内容是否相等。在Java中,默认实现是⽐较对象的引⽤,即⽐较两个对象是否指向内存中的相同位置。但通常,我们希望⽐较对象的内容是否相等
  • 鉴于这种情况,Object类中 equals() 方法的默认实现是没有实⽤价值的,所以通常都要重写
  • 而由于hashCode()与equals()具有联动关系(如果两个对象相等,则它们必须有相同的哈希码),所以equals()方法重写时,通常也要将hashCode()进⾏重写,使得这两个方法始终保持⼀致性。

    Pasted image 20241125165658.png
    Pasted image 20241125165712.png
    Pasted image 20241125165723.png
    Pasted image 20241125165746.png

重写equals一定要重写hashCode吗?

  • 如果仅仅是为了比较两个对象是否相等只重写equals就可以;
  • 如果你使用了hashSet、hashMap等容器,为了避免加入重复元素,或者查找元素,就一定要同时重写两个方法。
  • 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。
public String toString(){}

默认的 toString() 输出包名加类名和堆上的首地址

finalize()方法
线程中常用的方法
  • public final void wait(): 多线程中等待功能
  • public final native void notify() : 多线程中唤醒功能
  • public final native void notifyAll(): 多线程中唤醒所有等待线程的功能

Pasted image 20241125164322.png
Pasted image 20241125164338.png

ObjectsObject 区别

  • Objects 【public final class Objects extends Object】是Object 的工具类,位于java.util包。它从jdk1.7开始才出现,被final修饰不能被继承,拥有私有的构造函数。此类包含static实用程序方法,用于操作对象或在操作前检查某些条件。
    • null 或 null方法 ;
    • 用于计算对象的哈希代码;
    • 返回对象的字符串;
    • 比较两个对象,以及检查索引或子范围值是否超出范围
      Pasted image 20241125170144.png

2 == 与 equals()

综述:大部分情况等价,但某些类例外(如java.io.File、java.util.Date、java.lang.String、包装类(如java.lang.Integer和java.lang.Double类等))

  • equals()
    • 大部分情况下,当参数obj引用的对象与当前对象为同一个对象,就返回true,否则返回false;
    • 但,在JDK中有一些类覆盖了Object类的equals()方法,它们的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true。判断的是长的像不像。这些类包括:java.io.File、java.util.Date、java.lang.String、包装类(如java.lang.Integer和java.lang.Double类等)。
String str1=new String("Hello");
String str2=new String("Hello");
System.out.println(str1==str2); //打印false
System.out.println(str1.equals(str2)); //打印true
  • 当操作符 == 两边都是引用类型变量时,这两个引用变量必须都引用同一个对象,结果才为true。
Food fish1=new Fish();
Food fish2=new Fish();
Food fish3=fish1;
System.out.println(fish1==fish2); //打印false
System.out.println(fish1==fish3); //打印true

3 最终类、最终方法、常量

3.1 最终类、最终方法

  • 最终类:如果一个类没有必要再派生子类,通常可以用final关键字修饰,表明它是一个最终类

  • 最终方法:用关键字final修饰的方法称为最终方法。最终方法既不能被覆盖,也不能被重载,它是一个最终方法,其方法的定义永远不能改变

  • final类中的方法可以不声明为final方法,但实际上final类中的方法都是隐式的final方法

  • final修饰的方法不一定要存在于final类中。

  • 定义类头时,abstract和final不能同时使用

  • 访问权限为private的方法默认为final

3.2 常量

  • Java中的常量使用关键字 final 修饰。

  • final既可以修饰简单数据类型,也可以修饰复合数据类型。

  • final常量可以在声明的同时赋初值,也可以在构造函数

  • 复合数据类型常量可以是Java类库定义的复合数据类型,也可以是用户自定义的复合数据类型
    final double PI = 3.1415926

  • 简单数据类型常量其一旦确定,就不能被改变。

  • 复合数据类型常量指的是引用不能被改变,而其具体的值是可以改变的

  • 常量既可以是局部常量,也可以是类常量和实例常量。如果是类常量,在数据类型前加static修饰(由所有对象共享)。如果是实例常量,就不加static修饰

  • 常量名一般大写,多个单词之间用下划线连接。

局部常量、类常量、实例常量
  1. 局部常量(Local Constant): 局部常量是在方法、构造器或代码块内部定义的常量。它们只在定义它们的代码块内部有效,一旦代码块执行完毕,局部常量就不再存在。局部常量通常使用 final 关键字来声明,表示其值在初始化后不能被改变。
public void myMethod() {
    final int LOCAL_CONSTANT = 10; // 局部常量
    // 这里可以使用 LOCAL_CONSTANT
}
// 这里不能使用 LOCAL_CONSTANT,因为它只在 myMethod 方法内部有效
  1. 类常量(Class Constant): 类常量是在类的静态初始化块或静态成员变量中定义的常量。它们属于类本身,而不是类的实例。类常量也通常使用 final 关键字来声明,并且是 static 的,这意味着它们是类的所有实例共享的。
public class MyClass {
    public static final int CLASS_CONSTANT = 20; // 类常量
    // 这里可以使用 CLASS_CONSTANT
}
  1. 实例常量(Instance Constant): 实例常量是在类的非静态成员变量中定义的常量。它们属于类的每个实例,每个实例都有自己的实例常量副本。实例常量同样使用 final 关键字来声明,表示一旦被初始化,其值就不能改变。
public class MyClass {
    public final int INSTANCE_CONSTANT = 30; // 实例常量
    // 这里可以使用 INSTANCE_CONSTANT
}

Pasted image 20241125171136.pngPasted image 20241125171249.png

Pasted image 20241125171724.png
Pasted image 20241125171736.png
Pasted image 20241125171743.png

4 内部类

4.1 内部类的基本语法

内部类的分类

Pasted image 20241125171915.png

Pasted image 20241125172027.png
Pasted image 20241125172036.png

实例内部类

创建实例内部类的实例

在创建实例内部类的实例时,外部类的实例必须已经存在,例如要创建InnerTool类的实例,必须先创建Outer外部类的实例
两种语法:

  • Outer.InnerTool tool=new Outer().new InnerTool();
  • Outer outer=new Outer();
    Outer.InnerTool tool =outer.new InnerTool();
    以下代码会导致编译错误:Outer.InnerTool tool=new Outer.InnerTool();

实例内部类访问外部类的成员

  • 在内部类中,可以直接访问外部类的所有成员,包括成员变量和成员方法。
  • 实例内部类的实例自动持有外部类的实例的引用

Pasted image 20241125172451.png
Pasted image 20241125172455.png

静态内部类

  • 静态内部类的实例不会自动持有外部类的特定实例的引用
  • 在创建内部类的实例时,不必创建外部类的实例
    Pasted image 20241125184609.png
  • 客户类可以通过完整的类名直接访问静态内部类的静态成员
    Pasted image 20241125184707.pngPasted image 20241125184715.png

局部内部类

  • 局部内部类只能在当前方法中使用。
  • 局部内部类和实例内部类一样,可以访问外部类的所有成员
  • 此外,局部内部类还可以访问所在方法中的符合以下条件之一的参数和变量:
    最终变量或参数:用final修饰

Pasted image 20241125184928.png

4.2 内部类的用途

  • 封装类型:如果一个类只能由系统中的某一个类访问,可以定义为该类的内部类。
  • 直接访问外部类的成员
  • 回调外部类的方法

4.2.1 内部类封装类型

  • 顶层类只能处于public和默认访问级别
  • 而成员内部类可以处于public、protected、默认和private四个访问级别。
  • 此外,如果一个内部类仅仅为特定的方法提供服务,那么可以把这个内部类定义在方法之内

Pasted image 20241125190218.png
Pasted image 20241125190225.png
在客户类中不能访问Outer.InnerTool类,但是可以通过Outer类的getTool()方法获得InnerTool的实例

4.2.2 内部类访问外部类的成员

  • 内部类的一个特点是能够访问外部类的各种访问级别的成员

  • 假定有类A和类B,类B的reset()方法负责重新设置类A的实例变量count的值。一种实现方式是把类A和类B都定义为外部类
    Pasted image 20241125190908.pngPasted image 20241125190920.png

  • 假如需求中要求类A的count属性不允许被除类B以外的其他类读取或设置,那么以上实现方式就不能满足这一需求。

  • 在这种情况下,把类B定义为内部类就可以解决这一问题,而且会使程序代码更加简洁
    Pasted image 20241125191027.png

4.2.3 回调

Pasted image 20241125193007.png
Pasted image 20241125193023.png
故考虑使用回调方法
Pasted image 20241125193036.png
Pasted image 20241125193054.png
综上所述,回调实质上是指一个类尽管实际上实现了某种功能,但是没有直接提供相应的接口,客户类可以通过这个类的内部类的接口来获得这种功能。而这个内部类本身并没有提供真正的实现,仅仅调用外部类的实现
可见,回调充分发挥了内部类具有访问外部类的实现细节的优势。
Pasted image 20241125193201.png

4.3 内部类的文件

对于每个内部类,Java编译器会生成独立的.class文件。这些类文件的命名规则如下:

  • 成员内部类:外部类的名字$内部类的名字
  • 局部内部类:外部类的名字$数字和内部类的名字
  • 匿名类:外部类的名字$数字
    Pasted image 20241125193304.png

5 匿名类

  • 匿名类就是没有名字的类,是将类和类的方法定义在一个表达式范围里。
  • 匿名类本身没有构造方法,但是会调用父类的构造方法
  • 匿名内部类将内部类的定义与生成实例的语句合在一起,并省去了类名以及关键字“class”,”extends”和“implements”等
例1
import javax.swing.*; // 导入Swing包
import java.awt.*; // 导入AWT包
import java.awt.event.*; // 导入事件处理包

public class J_Test1 extends JFrame { // 定义J_Test1类,继承自JFrame
    public J_Test1() { // 构造函数
        super("Test anonymous inner class"); // 调用父类构造函数,设置窗口标题
        Container container = getContentPane(); // 获取内容面板
        container.setLayout(new FlowLayout(FlowLayout.CENTER)); // 设置布局为流式布局,居中对齐
        JButton b = new JButton("Press me"); // 创建按钮
        container.add(b); // 将按钮添加到内容面板
        b.addActionListener( // 为按钮添加事件监听器
            new ActionListener() { // 匿名内部类实现ActionListener接口
                public void actionPerformed(ActionEvent e) { // 实现actionPerformed方法
                    System.out.println("The button is pressed"); // 打印消息
                }
            }
        );
        setSize(100, 80); // 设置窗口大小
        setVisible(true); // 设置窗口可见
    }

    public static void main(String[] args) { // main方法
        J_Test1 application = new J_Test1(); // 创建J_Test1对象
        application.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置窗口关闭操作
    }
}
import javax.swing.*; // 导入Swing包
import java.awt.*; // 导入AWT包
import java.awt.event.*; // 导入事件处理包

public class J_Test2 extends JFrame { // 定义J_Test2类,继承自JFrame
    public J_Test2() { // 构造函数
        super("Test anonymous inner class"); // 调用父类构造函数,设置窗口标题
        Container container = getContentPane(); // 获取内容面板
        container.setLayout(new FlowLayout(FlowLayout.CENTER)); // 设置布局为流式布局,居中对齐
        JButton b = new JButton("Press me"); // 创建按钮
        container.add(b); // 将按钮添加到内容面板
        b.addActionListener(new J_ActionListener()); // 为按钮添加事件监听器,使用外部类J_ActionListener
        setSize(100, 80); // 设置窗口大小
        setVisible(true); // 设置窗口可见
    }

    public static void main(String[] args) { // main方法
        J_Test2 application = new J_Test2(); // 创建J_Test2对象
        application.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置窗口关闭操作
    }
}

class J_ActionListener implements ActionListener { // 定义J_ActionListener类,实现ActionListener接口
    public void actionPerformed(ActionEvent e) { // 实现actionPerformed方法
        System.out.println("The button is pressed"); // 打印消息
    }
}

Pasted image 20241125185833.png

例2

Pasted image 20241125185906.png
Pasted image 20241125185945.png

Built with MDFriday ❤️