☕ 第四章 面向对象(下)

4.5 Object类

📄 一句话概括

Object 是所有类的祖宗——你写的任何类不写 extends,都默认继承 Object。它送每个对象三件天生自带的工具:toString(打印时说人话)、equals(判断相不相同)、hashCode(住址编号)。三件默认版都很"敷衍",实际开发里 toString 和 equals 十有八九要重写。

1. 收网:为什么空类也有 toString()?

4.1 留过一个谜:你写的空类,什么方法都没定义,sout(对象) 却不报错——本节给答案:

class Dog { }              // 什么都不写的类

// 编译器等价于:
class Dog extends Object { }

任何类不写 extends,自动继承 Object。所以每个对象天生拥有 Object 的全部方法——toString()、equals()、hashCode() 都是祖传的。4.1 说"Object 万物之祖",今天正式认识这位祖宗。

这也带来一个多态的终极应用:

Object box = new Dog();      // Object 钥匙能开任何车
Object box2 = "你好";        // String 也是 Object 的子孙
Object box3 = 42;            // 连 42 都行(包装类,第 6 章揭晓)

万物皆 Object——你天天调的 System.out.println(x) 能打印一切类型,就是因为它的参数类型是 Object。

2. toString():解开"类名@乱码"之谜

那串乱码是怎么来的

打印对象时你大概见过这个:

System.out.println(dog);    // 输出:Dog@1b6d3586  ← 这是什么鬼?

真相(上面那张图的暗道):

println(dog) 内部自动执行了 dog.toString()
→ dog 没重写 toString,执行的是 Object 祖传默认版
→ 默认版返回:类名 + @ + 哈希值的十六进制(约等于"住址编号")
→ 所以屏幕上是 Dog@1b6d3586——不是乱码,是"类名@住址"

注意两点:① println(对象) 隐式调 toString,println(dog.toString()) 结果一模一样;② 运行的是对象自己的版本——4.4 的"运行看右边"在这里上岗。

重写 toString:让对象说人话

class Dog {
    String brand;
    int age;

    @Override
    public String toString() {
        return "Dog{brand='" + brand + "', age=" + age + "}";
    }
}

System.out.println(dog);   // 输出:Dog{brand='旺财', age=3}  ← 这才是人话
💡 IDEA 一键生成

Alt + Insert(Windows)→ 选 toString() → 勾选属性 → 确定。生成完顺手补上模板没给的业务字段即可。重写时务必带上 @Override 注解——拼错方法名它会当场报错,防"重写了个寂寞"。

3. equals():判断"相同",比的是内容还是地址?

默认版:和 == 一模一样

Dog d1 = new Dog();
Dog d2 = new Dog();
System.out.println(d1.equals(d2));   // false!明明都是狗

Object 祖传的 equals 源码就一行:return this == obj;——默认比的是地址(两个 new 就是两个住址),不是内容。

== 和 equals 的完整对比表(本节考试中心)

写法 基本类型(int 等) 引用类型(对象)
== 比值(8 == 8 → true) 比地址(是不是同一个对象)
equals 不能用(编译报错) 默认比地址,重写后比内容

一句话:基本类型比值用 ==;对象比内容用 equals(前提是重写过)。

String 是个特例,也是个大坑

String 已经替你重写过 equals(比内容),但很多人栽在 == 上:

String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");

s1 == s2          // true!字面量走"常量池",两把钥匙指向同一份
s1 == s3          // false!new 造了新地址
s1.equals(s3)     // true——比内容,永远对
🚨 铁律:比较字符串内容,永远用 equals,永远别用 ==

== 有时对有时错(取决于字符串怎么来的),靠它写出的 bug 最难查。从今天起养成肌肉记忆。

自定义类重写 equals

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;              // 自己和自己,直接同
    if (!(obj instanceof Dog)) return false;   // 不是狗,不比
    Dog other = (Dog) obj;                     // 4.4 的向下转型验货
    return this.age == other.age && this.brand.equals(other.brand);
}
💡 老规矩,IDEA 代劳

Alt + Insert → equals() and hashCode() 一键生成。生成版逻辑严密,读懂它比你手写收获大。

4. hashCode():混个脸熟,谜底留到集合

hashCode 返回对象的"住址编号"(int)。现在只需要记住一条规矩:

重写了 equals,就必须重写 hashCode——规则:两个对象 equals 为 true,它们的 hashCode 必须相同。

为什么?HashSet/HashMap(第 6 章集合)先按 hashCode 找格子,再用 equals 确认——如果两个"内容相同"的对象编号不同,就会被当成两个东西存两份。谜底到集合章正式收网,今天记住这条搭伴规矩就行。

5. 动手:给 Dog 装上"人话"和"识狗眼"

在 IDEA04 里新建 TestObject:

class Dog {
    String brand;
    int age;

    public Dog(String brand, int age) {
        this.brand = brand;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{brand='" + brand + "', age=" + age + "}";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Dog)) return false;
        Dog other = (Dog) obj;
        return age == other.age && brand.equals(other.brand);
    }
}

public class TestObject {
    public static void main(String[] args) {
        Dog d1 = new Dog("旺财", 3);
        Dog d2 = new Dog("旺财", 3);
        Dog d3 = new Dog("小黑", 2);

        System.out.println(d1);                    // 重写前:Dog@xxx;重写后:Dog{brand='旺财', age=3}
        System.out.println(d1.equals(d2));         // true(内容相同)
        System.out.println(d1 == d2);              // false(两个 new 两个地址)
        System.out.println(d1.equals(d3));         // false(内容不同)
        System.out.println("abc".equals(new String("abc")));   // true——String 早替你重写好了
    }
}

成功标准:5 行输出依次是——人话版对象描述、true、false、false、true。第 2、3 行对照着看,"equals 比内容、== 比地址"就长这样。

三个 30 秒实验

  1. 删掉 toString 重写再跑——看 Dog@1b6d3586 乱码回归,确认"乱码 = 默认版 toString"。
  2. 删掉 equals 重写再跑——d1.equals(d2) 从 true 变 false,亲眼见默认版比地址。
  3. 拿 int 用 equals——写 int x = 8; x.equals(8); 看编译报错"int 不能被解引用"——基本类型只能用 ==,表格第一列长这样。

6. 常见误区

⚠️ 误区 1:equals 默认就比较内容

恰恰相反——Object 默认版就是 ==,比地址。String 让你产生"equals 天生比内容"的错觉,那是因为 String 重写过。自定义类不重写,equals 就是 ==。

⚠️ 误区 2:基本类型也能用 equals

int 没有方法,x.equals(8) 编译直接报错。基本类型只有 == 一条路(包装类出场后才有 equals 可用)。

⚠️ 误区 3:`s1 == s2` 为 true,说明 == 也能比字符串内容

那 true 是常量池的巧合(两个变量恰好指向同一份"abc"),不是 == 在比内容。换个写法 new String("abc") 它就翻脸——别赌巧合,永远用 equals。

⚠️ 误区 4:重写 equals 不用管 hashCode

编译器确实不拦,但集合章等着收你:内容相同的两个对象被存进 HashSet 当成两个元素——逻辑错误,程序照跑,结果不对。规矩今天立下:equals 和 hashCode 必须搭伴重写。

⚠️ 误区 5:toString 只有打印时才有用

字符串拼接("信息:" + dog)同样隐式调 toString;日志框架打对象、调试时看变量值,背后全是它。重写 toString 是性价比最高的重写,任何实体类都值得配一个。

7. 自测题(先自己答,再展开)

❓ Q1:为什么你写的类不定义任何方法,也能调 toString()?

任何类不写 extends 都默认继承 Object,toString/equals/hashCode 是 Object 的祖传方法,每个对象天生自带。

❓ Q2:`System.out.println(dog)` 打出 `Dog@1b6d3586`,这串东西怎么来的?怎么让它打"人话"?

println 隐式调用了 dog 的 toString(),默认版(Object 提供)返回"类名@哈希值的十六进制"。在 Dog 里重写 toString(带 @Override),返回属性拼接的可读字符串即可;或 IDEA Alt+Insert 一键生成。

❓ Q3:`==` 和 `equals` 分别比什么?

==:基本类型比值,引用类型比地址(是不是同一个对象)。equals:不能用于基本类型;引用类型默认比地址,重写后比内容。口诀:基本类型比值用 ==,对象比内容用 equals。

❓ Q4:`String s1 = "abc"; String s2 = new String("abc");` s1 == s2 和 s1.equals(s2) 各是多少?为什么?

s1 == s2 是 false(new 造了新地址,s1 指向常量池、s2 指向堆里新对象);s1.equals(s2) 是 true(String 重写过 equals,比内容)。结论:字符串内容比较永远用 equals。

❓ Q5(毕业题):写出"重写了 equals 就必须重写 hashCode"的理由,并说明它现在记住多少、以后哪收网

规则要求:equals 相同的两个对象,hashCode 必须相同。将来 HashSet/HashMap 这类集合先按 hashCode 定位格子、再用 equals 确认是否相同——只重写 equals 不重写 hashCode 时,"内容相同"的两个对象编号不同,会被当成两个元素存两份(去重失效、查找出错)。今天只需记住搭伴规矩,具体机理第 6 章集合(HashSet/HashMap)收网。

8. 下一步学什么


相关笔记