java篇 类的进阶0x10:万类之祖:Object 类

文章目录

  • 万类之祖:Object 类
    • `hashCode()` 与 `equals()`
      • `hashCode()` 方法
      • `equals()` 方法
        • `==` vs. `equals()`
        • String 的 `equals()`
      • 为什么要重写 hashCode 和 equals 方法
        • 重写(覆盖)前 `hashCode()` 和 `equals()` 的作用
        • 什么情况下需要重写(覆盖) `hashCode()` 和 `equals()` ?
        • 为什么重写`equals()` 方法一定要重写 `hashCode()` 方法
      • 自动覆盖 `equals()` 与 `hashCode()`
    • `toString()` 方法
      • 自动覆盖 `toString()`
  • Objects 类

万类之祖:Object 类

所有的类(除了 Object 类本身),都间接或者直接地继承自 Object 类。

Object 类只有成员方法,没有成员变量。

在 IDEA 中:菜单栏 --> Navigate --> Type Hierarchy (或直接 ctrl + H

可以看到当前类的类继承关系。

可以看到,无论是哪条类继承链,最上方总会是 Object 类( java.lang.Object)。

这也是为什么在 IDEA 中写代码时,经常在输入点操作符(.)后,会弹出一大堆的不是我们自己编写的方法提示(/推荐)让我们选择,因为这些弹出来推荐使用的方法,都是 Object 类中定义的,而我们自己编写的类所创建的对象,自然会继承这些方法。

我们虽然没有显式在自定义类中 extends Object,但 java 会给我们自动补上 extends Object。当然你自定义一个类,非自己写上比如说 public class TestUse extends Object 也没有问题,测试过并不会报错。但 IDEA 会提示你可以删除冗余的 extends Object

package org.test.extendstest.statictest;

public class TestUse {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("Object 类的实例信息:"+obj); // Object 类的实例信息:java.lang.Object@404b9385
        A a = new A();
        System.out.println("打印 A 类的实例信息"); // 打印 A 类的实例信息
        printObj(a);
    }

    public static void printObj(Object obj){
        System.out.println(obj);
		// org.test.extendstest.statictest.A@682a0b20
		
        System.out.println(obj.toString());	
        // org.test.extendstest.statictest.A@682a0b20
        
        System.out.println(obj.getClass());
        // class org.test.extendstest.statictest.A
        
        System.out.println(obj.hashCode());
        // 1747585824

		// 1747585824 转换成 16 进制就是 682a0b20
        }
}

代码详细解析如下:

package org.test.extendstest.statictest;

public class TestUse {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("Object 类的实例信息:"+obj);
        
        // 当然,null 直接用 System.out.println(null) 是可以正常输出 “null” 的。
//        printObj((Object)null);					// 如果不注释会报错,因为 null 无法调用 .toString、.getClass、.hashCode 函数
//        printObj(null);							// 如果不注释会报错,因为 null 无法调用 .toString、.getClass、.hashCode 函数
        
        A a = new A();
        System.out.println("打印 A 类的实例信息");
        printObj(a);
    }

    public static void printObj(Object obj){
        System.out.println(obj);
        // 其实在 IDEA 中 ctrl 点击 println 进去看代码,会发现经过相关调用跳转,最终调用的函数是:
        /*
        public static String valueOf(Object obj) {
        	return (obj == null) ? "null" : obj.toString();
    	}
         */
        // 也就是会判断是不是 null ,是 null 就输出 “null”,不是的话就调用 .toString(),所以输出和下方的 .toString() 一样。

        // 看看实现
        System.out.println(obj.toString());		
        // 查看 .toString() 源码
        /*
        public String toString() {
        	return getClass().getName() + "@" + Integer.toHexString(hashCode());
    	}
         */
        // 可以看到输出的是一个拼接的字符串,分别就有调用下方的 .getClass() 和 .hashCode()
        // 这里 .toHexString() 就是转 16 进制,也就是把 hashCode() 返回的哈希值转成 16 进制
        

        // native 方法。
        // java 的类库里有许多 native 方法,native 的意思是这个方法没有方法体,它方法的代码实际上是用本地(操作系统,平台相关,比如只能跑在 windows 或只能跑在 mac OS 上)的代码实现的(C 或 C++)。它存在一个映射机制,根据方法名,对应到本地的 c/c++ 写的和平台相关的方法。
        System.out.println(obj.getClass());			
        System.out.println(obj.hashCode());
        /*
        @IntrinsicCandidate
    	public final native Class getClass();		// java 源码中没有方法体(方法体在计算机的某个 dll 中定义)
         */
        /*
        @IntrinsicCandidate
    	public native int hashCode();					// java 源码中没有方法体(方法体在计算机的某个 dll 中定义)
         */
        
    }
}

// 输出结果:
// Object 类的实例信息:java.lang.Object@404b9385
// 打印 A 类的实例信息
// org.test.extendstest.statictest.A@682a0b20
// org.test.extendstest.statictest.A@682a0b20
// class org.test.extendstest.statictest.A
// 1747585824

// 1747585824 转换成 16 进制就是:682a0b20
// 哈希码是一个标识,相对比较唯一地去标识一个对象(但不确保完全唯一,因为存在哈希碰撞,两个对象刚好同一个哈希值,但只要是哈希值(哈希码)不同,那对象就肯定是不同的)

hashCode()equals()

参考:

  • https://blog.csdn.net/qq_50838572/article/details/122877342

hashCode()equals() 这两个基本上是初级 java 程序员面试必考的内容。

hashCode()equals() 是最常覆盖的两个方法。覆盖原则是:equals()truehashCode 就应该相等。这是一种约定俗成的规范。

  • equals()truehashCode() 相等的充分不必要条件;
  • hashCode() 相等是 equals()true 的必要不充分条件。

e q u a l s ( ) 是 t r u e ⇒ h a s h C o d e ( ) 相等 equals() 是 true\Rightarrow hashCode() 相等 equals()truehashCode()相等

h a s h C o d e ( ) 相等 ⇏ e q u a l s ( ) 是 t r u e hashCode() 相等 \nRightarrow equals() 是 true hashCode()相等equals()true

hashCode() 方法

hashCode 可以翻译为 “哈希码”,或者 “散列码”,是一个表示对象的特征值的 int 整数。

当没有任何类去覆盖 hashCode() ,可以简单地认为哈希码的值就是这个对象在内存中的地址(Object 中 hashcode() 是根据对象的存储地址转换而形成的一个哈希值)。

// Object 的 hashCode() 是 native 方法,因此在 java 源码中是没有方法体的。它本质上是用 C++ 来实现的。
@IntrinsicCandidate
public native int hashCode();

equals() 方法

equals 方法应该用来判断两个对象从逻辑上是否相等。(并不是判断这两个对象是不是同一个对象)

这里之所以是“应该”,是因为如果不覆盖的 equals() ,那么就是继承 Object 类中的 equals() 方法,但这个原始的 equals() 就真的仅仅是在判断两个对象是不是同一个对象。这样是没什么意义的,所以要通过覆盖,让 equals() 真正去判断两个对象在业务逻辑上是否相等。

== vs. equals()

== 在引用数据类型当中进行的是地址的比较,equals() 方法在 Object 类当中其底层也是用 == 比较地址,但是不同的类可能会重写equals() 方法,比如 String 类中的 equals() 方法就是先比较地址是否相同,如果相同直接返回 true,地址不同再比较值,如果值相等那么同样返回 true。

// Object 类中的 equals()
public boolean equals(Object obj) {
        return (this == obj);
}

// String 类中覆盖的 equals()
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
        && (!COMPACT_STRINGS || this.coder == aString.coder)
        && StringLatin1.equals(value, aString.value);
}

String 的 equals()

因为在 java 中 String 类实在用得太多了,对象创建也太多了,所以 java 针对 String 添加了一些优化。

public class StringEqualsTest {
    public static void main(String[] args) {
        String s1 = "aaabbb";
        String s2 = "aaa" + "bbb";
        System.out.println("用 == 判断结果:"+(s1 == s2));			// 用 == 判断结果:true
        System.out.println("用 equals 判断结果:"+ s1.equals(s2));	// 用 equals 判断结果:true
    }
}

这个输出结果可能会让人惊讶。因为按道理,== 比较的是对象的实际地址,也就是判断两个引用对象是否是同一个对象,而字符串是不可变的,拼接实际上是创建一个新的对象,因此 s1 与 s2 本应该是两个不同的对象,但输出却不是 false 而是 true。

这是因为 java 对 String 类对象做了特殊的优化:

  • java 会有一个专门的地方用来放字符串,如果创建的字符串不是特别长,而且整个程序运行的时候,字符串创建也没有太多时,就把这些创建的字符串放到一个地方。当你创建一个新的字符串时,Java 会先去那个地方去找,看有没有一样的字符串,如果有的话,就直接返回这个字符串的引用,而不重新创建一个字符串。

    这是因为本来 String 类的对象就是不可变的,你即便创建一个新的 String 对象,和沿用之前创建的(值一样的)是没有任何区别的。所以 java 可以放心地做这种事情。

    所以 s1 和 s2 实际上指向的就是同一个对象。

    但 java 对 String 的优化也是有限制的,如果太长了,突破了这个限制,比如创建的 s1 很长很长,s2 也很长很长,那么即便已经创建了 s1,s2 的值和 s1 一样,但仍旧会给 s2 重新创建一个新的对象。此时,用 == 判断,则 s1 与 s2 是不相同的。但用 equals() 判断,因为值实际是一样的,所以 s1 与 s2 是相等的。

    Kevin:对上面这个“限制”的说法,Kevin 表示存疑。因为实际上试过很长很长的(IDEA中String声明中赋值的常量字符串最多不超过65535字符,否则报错:常量字符串过长。所以就用了极限不报错的长度),但仍然 == 的结果为 true。

    再次尝试测试代码如下:

    import java.util.Scanner;
    
    public class StringEqualsTest {
        public static void main(String[] args) {
            String s1 = "aaabbb";
            String s2 = "aaa" + "bbb";
            System.out.println("用 == 判断结果:"+(s1 == s2));            // true
            System.out.println("用 equals 判断结果:"+ s1.equals(s2));    // true
    
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入 s3:");
            String s3 = scanner.nextLine();     // 输入 aaabbb
            System.out.println("请输入 s4:");
            String s4 = scanner.nextLine();     // 输入 aaabbb
            System.out.println("用 == 判断结果:" + (s3 == s4));          // false
            System.out.println("用 equals 判断结果:" + s3.equals(s4));   // true
            System.out.println("s1 == s3:"+(s1==s3));                   // false
            System.out.println("s1.equals(s3):"+s1.equals(s3));         // true
    
            String s5 = new String("aaabbb");
            String s6 = new String("aaabbb");
            System.out.println("用 == 判断结果:" + (s5 == s6));          // false
            System.out.println("用 equals 判断结果:" + s5.equals(s6));   // true
            System.out.println("用 == 判断结果:" + (s1 == s5));          // false
            System.out.println("用 equals 判断结果:" + s1.equals(s5));   // true
            System.out.println("用 == 判断结果:" + (s3 == s5));          // false
            System.out.println("用 equals 判断结果:" + s3.equals(s5));   // true
            
    		StringBuilder s7 = new StringBuilder("aaa");
            StringBuilder s8 = new StringBuilder("aaa");
            s7.append("bbb");
            s8.append("bbb");
            System.out.println(s7==s8);									// false
            System.out.println(s7.toString()==s8.toString());			// false
            System.out.println(s7.toString().equals(s8));				// false
            System.out.println(s7.toString().equals(s8.toString()));	// true
            System.out.println(s7.toString()==s1);						// false
            System.out.println(s7.toString().equals(s1));				// true
    
        }
    }
    

    发现,只要是通过 Scanner 输入的字符串、或者new String() 创建的字符串,无论多短,即便值相同,也不会是同一个对象,即 == 永远为 false。

    Kevin:所以一开始的 s1 与 s2 之所以相等,和字符串长度应该没有关系,当然这也的确有 java 对 String 类的优化在里面。但这里主要应该是因为String s1 = "aaabbb";"aaabbb"是显式字符串常量(而不被认为是对象,对象应该放于堆中),存放在常量池里。但 new String("aaabbb")存放在内存的堆中。而无论是 new String()new StringBuilder() 还是 Scanner的 nextLine() 实际上都创建了新的 String 对象,只是对象中存放的值相等罢了,所以 java 对 String 的优化仅仅是用在了 “显式字符串常量” 上,而与字符串长短无关。

鉴于这种不统一的表现,用 == 来判断两个引用指向的字符串是否相等是不靠谱的。

因此在实际判断两个 String 类对象是否相等,应该使用 equals() 而非 ==

为什么要重写 hashCode 和 equals 方法

重写(覆盖)前 hashCode()equals() 的作用

重写前,equals()hashCode() 源代码如下(由于 hashCode() 是 native method,所以在 java 源代码中没有方法体):

// Object 类中的 equals(),效果是直接判断两个对象是否为同一个对象
public boolean equals(Object obj) {
        return (this == obj);
}

// Object 类中的 hashCode(),效果是根据对象的存储地址转换为一个哈希值(int 类型值)
@IntrinsicCandidate
public native int hashCode();

什么情况下需要重写(覆盖) hashCode()equals()

可以看到,Object 的 equals() 真的没什么意义,仅仅是判断这个对象是不是它自己,而业务中,我们通常是需要对比的是两个不同的对象,比较它们的业务逻辑意义上是否相同,即它们包含的数据是否相同。如果相同则判断它们相同,所以,equals() 需要根据程序员自己的业务逻辑去覆盖,仅仅沿用 Object 中定义的 equals() 意义不大。

同样,Object 中的 hashCode() 是根据对象的实际地址映射出来的一个 int 整数,如果用 hashCode() 来判断两个对象是否相同也没有太大意义。因为除去有可能不同对象发生哈希碰撞,判断的仍旧仅仅是这个对象是否是它自身。

所以“什么情况下需要重写(覆盖) hashCode()equals() ?”的答案是:根据具体业务需求来重写这两个方法。

为什么重写equals() 方法一定要重写 hashCode() 方法

那么问题来了,如果现在想比较两个不同对象中包含的数据是否一致,如果一致,就判断两个对象相同,那么似乎只重写 equals() 就够了,在 equals() 中把两个对象的所有成员变量(或者业务逻辑中重视的那几个成员变量,即可以不将所有成员变量一一对比,而只对比你想对比的并依比较结果判断两个对象在业务逻辑上是否相等)挨个比较一下不就可以了吗?为什么还得把 hashCode() 重写呢?

原因是这样的:

  • 很多时候,我们的需求不是光判断这两个对象所包含的数据是否相同,然后仅仅作出一个对象是否相同的判断。这个行为背后是有下一步的,比如说:在 java 的一个容器里,容器中的对象是不允许重复的,而是否重复,就是由 hashCode 来判断。比如 HashSet 或者 HashMap 的 key,是不允许重复的。现在的场景可能是,我需要往一个这样的容器中放入对象,如果对象是和容器中已经存在的对象相同,我就让它覆盖,如果不同,那就正常放入这个新的对象。

    比如说容器里的对象只有一个 age 属性,现在存在 A 对象和 B 对象,A.age = 13; B.age = 13,那么这两个对象就应该判断为相同的(通过重写 equals()),但是如果不重写 hashCode() ,那么这两个对象因为内存地址是不同的,也就是说 hashCode() 的返回值是不同的,就会被 java 认为是不同的两个对象,可以同时放入到这个容器中,而不会发生覆盖行为。

    HashMap: 是一个散列表,它存储的内容是键值对(key-value)映射。该类实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

    HashSet: 可以认为 HashSet 是简化版的 HashMap,但存储的内容不是键值对(key-value)映射,可以认为 HashSet 仅仅实现了 key 的部分。该类实现了 Set 接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为 null 的元素,但最多只能一个。

下面看一个只重写 equals() ,而不重写 hashCode() 的例子:

// 只重写 equals() ,而不重写 hashCode()
public class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }
    
	// 重写 equals()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Dog)) return false;
        Dog dog = (Dog) o;
        return name.equals(dog.name);
    }

    @Override
    public String toString() {
        return "{ Dog: " + name + " }";
    }

}

// 调用类
import java.util.HashSet;

public class TestUse {
    public static void main(String[] args) {
        Dog a = new Dog("Tom");
        Dog b = new Dog("Tom");
        Dog c = new Dog("Jerry");
        System.out.println(a == b);				// false
        System.out.println(a.equals(b));		// true
        System.out.println(a == c);				// false
        System.out.println(a.equals(c));		// false

        HashSet hashset = new HashSet();
        hashset.add(a);
        hashset.add(b);
        hashset.add(c);
        System.out.println(hashset);			// [{ Dog: Jerry }, { Dog: Tom }, { Dog: Tom }]
    }
}
// 输出结果:
// false
// true
// false
// false
// [{ Dog: Jerry }, { Dog: Tom }, { Dog: Tom }]

// 这样就很别扭了,a 和 b 应该是当作同一只狗的,但方法 hashset 里却没有覆盖掉。

下面再看一个同时重写了 equals()hashCode() 的例子:

import java.util.Objects;

public class Dog {
    private String name;

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

	
    // 重写 equals()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Dog)) return false;
        Dog dog = (Dog) o;
        return name.equals(dog.name);
    }

    // 重写 hashCode()
    // 现在一般都不自己重写 hashCode(),都把它交给 IDEA 去自动生成,因为 hashCode 已经有一套相对标准的生成流程了,我们只需要让 IDEA 帮我们生成就好。
    @Override
    public int hashCode() {
        return Objects.hash(name);		// 用一个静态方法 Objects.hash()来生成哈希码
    }

    @Override
    public String toString() {
        return "{ Dog: " + name + " }";
    }
}

// 调用类
import java.util.HashSet;

public class TestUse {
    public static void main(String[] args) {
        Dog a = new Dog("Tom");
        Dog b = new Dog("Tom");
        Dog c = new Dog("Jerry");
        System.out.println(a == b);				// false
        System.out.println(a.equals(b));		// true
        System.out.println(a == c);				// false
        System.out.println(a.equals(c));		// false

        HashSet hashset = new HashSet();
        hashset.add(a);
        hashset.add(b);
        hashset.add(c);
        System.out.println(hashset);			// [{ Dog: Tom }, { Dog: Jerry }]
    }
}
// 输出结果:
// false
// true
// false
// false
// [{ Dog: Tom }, { Dog: Jerry }]

// 这样一来,就可以覆盖掉相同的对象了,业务逻辑就变得合理了。

自动覆盖 equals()hashCode()

其实在 IDEA 中,可以直接让 IDEA 自动生成覆盖的 equals()hashCode()

  • 源代码空白处鼠标右击 --> Generate...(Alt + Insert) --> equals() and hashCode()

    里面的选项见到的都打勾就好了。

toString() 方法

toString() 就是把类里的信息(通过生成 String)描述出来。

自动覆盖 toString()

其实在 IDEA 中,可以直接让 IDEA 自动生成覆盖的 toString()

  • 源代码空白处鼠标右击 --> Generate...(Alt + Insert) --> toString()

    选择要描述的属性(可以选择其中几个或全选),然后点击 OK

// 自动生成的 toString() 类似下面格式:
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +  // 其实这里的 name(指标黑色的成员变量)其实就是调 toString() 方法得到一个 String,然后拼接。
'}';
}

// 如果这些属性本身是引用类型,其实就是调用它们的 toString 方法返回一个 String,然后再把这些 String 拼接。
// 或许在 java 程序中显式地看到 toString() 的调用并不多,但实际上对 toString(默默地调用还是很多的。

toString() 方法不仅仅是在调用的时候有用,在 debug 时,IDEA 其实也会调用对象的 toString() 方法帮我们获取到这个对象的状态信息并展示出来,方便我们调试。(如 debug 时,显示在每行运行过的源代码右边的灰色的信息,其实调用的就是 toString(),底部的监控信息也是调用的 toString(),当然下方的对象信息还可以进一步点击左方的尖括号展开看对象信息,但没展开前实际上就是调用的 toString(),和我们定义的 toString() 的格式一致,帮助我们看到有数据的类的 实例/对象 里面是什么数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K80NTX0G-1690384807991)(10%20万类之祖:Object%20类.assets/image-20230115115704229.png)]

public class Dog {
    private String name;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Dog)) return false;
        Dog dog = (Dog) o;
        return name.equals(dog.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }


	// 覆盖定义的 toString()
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

Objects 类

Objects 类与 Object 类是不同的,要注意区别。查看 Objects 类的继承关系,会发现 Objects 类也是继承自 Object 类的。

Object(java.lang)
	|
	|--- Objects(java.util)

使用 java.lang 包的类是无需导入的,但使用 java.util 包里的类是需要 import 导入的。

再看看两个相似但不同的函数:equals()

// Objects 类里的 equals()
public static boolean equals(Object a, Object b) {  
    return (a == b) || (a != null && a.equals(b));  
}



// Object 类里的 equals()
public boolean equals(Object obj) {  
    return (this == obj);  
}

可以看到 Objects 类里的 equals() 是静态方法,而 Object 类里的 equals() 是实例方法。

而且从程序逻辑来说,Object 的 .equals() 是判断两个对象是不是同一个对象(使用 ==),而 Objects.equals() 则是会判断两个对象是不是同一个对象或非空的情况下调用 Object 的 .equals()

你可能感兴趣的:(java,java)