Object中的类与方法

toString

Java中的Object类是所有类的超类,其toString()方法是一个基础但关键的方法,用于返回对象的字符串表示。以下从定义、默认行为、重写机制及实际应用等方面详细解析:

一、toString()方法的定义与默认行为
1. 默认实现
   Object类中toString()的默认实现返回字符串格式为:类名@哈希码(例如ClassName@1a2b3c4d)。其源码定义为:  

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

 这一设计主要用于标识对象的内存地址,但缺乏对对象实际内容的描述。

2. 调用场景 
   当直接输出对象引用(如System.out.println(obj))或显式调用obj.toString()时,均会触发该方法。

package oop.DATE;

public class Date {
    int year;
    int month;
    int day;

    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}
package oop.DATE;

public class DateText {
    public static void main(String[] args) {
        Date a1 = new Date(2025, 3, 14);
        String s1 = a1.toString();
        System.out.println(s1); // oop.DATE.Date@2f4d3709
    }
}

二、toString()方法的重写与意义
1. 为何需要重写
    - 可读性:默认的哈希码无法直观反映对象内容,重写后可提供更清晰的业务信息(如用户对象的姓名、ID等)。  
    - 调试便利性:日志或调试时,通过toString()可直接查看对象状态,无需逐字段检查。

2. 常见类的重写示例
   - String类:返回字符串本身的值(如`"Hello"`)。  
   - Date类:返回日期时间格式(如`"Fri Mar 14 10:00:00 CST 2025"`)。  
   - 包装类:如Integer.toString(5)返回`"5"`。

    @Override
    public String toString() {
        return year + "-" + month + "-" + day;
    }
package oop.DATE;

public class DateText {
    public static void main(String[] args) {
        Date a1 = new Date(2025, 3, 14);
        String s1 = a1.toString();
        System.out.println(s1);// 2025-3-14
    }
}

 3. 自定义类的重写方法
   - 手动重写:根据业务需求拼接字段信息。  
   - IDE生成:通过IDE(如IntelliJ或Eclipse)的快捷键自动生成包含所有字段的`toString()`方法, 这种方式既高效又减少人为错误。

注意一下源码:

    public void println(Object x) {
        String s = String.valueOf(x);
        if (getClass() == PrintStream.class) {
            // need to apply String.valueOf again since first invocation
            // might return null
            writeln(String.valueOf(s));
        } else {
            synchronized (this) {
                print(s);
                newLine();
            }
        }
    }
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

由此,我们直接调用toString与System.out.println()是不一样的,如:

package oop.DATE;

public class DateText {
        Date a2 = null;
        // 当我们在System.out.println()中传入一个引用参数时,自动调用toSpring
        System.out.println(a2); // null
        System.out.println(a2.toString()); // NullPointerException
    }
}

此时就会出现空指针异常 

三、`toString()`的扩展应用与最佳实践
1. 多态与继承  
   子类可重写父类的`toString()`,结合`super.toString()`复用父类逻辑。

2. 静态工具方法
   某些类(如`Integer`)提供静态`toString()`方法,支持参数转换,例如:  

   Integer.toString(12); // 返回"12"
   Integer.toString(10, 2); // 返回二进制字符串"1010"

3. 最佳实践建议
   - 覆盖toString():所有业务类建议重写该方法,提升代码可维护性。  
   - 避免敏感信息:若对象包含密码等敏感字段,需在`toString()`中过滤。  
   - 性能优化:频繁调用的场景(如日志记录)需注意字符串拼接效率,可使用`StringBuilder`。

四、总结
`toString()`是Java对象描述的核心方法,默认实现仅提供基础标识,而通过重写可增强其表达能力和实用性。合理使用该方法能显著提升代码可读性和调试效率,是面向对象编程中不可忽视的细节。

equals

equals() 方法是对象比较的基础方法。

 默认行为:引用比较

  • Object 类中的 equals() 方法默认实现是通过 == 运算符比较两个对象的内存地址,即判断两个引用是否指向同一个对象。原码为:
        public boolean equals(Object obj) {
            return (this == obj);
        }
    package oop.DATE;
    
    public class DateText {
        public static void main(String[] args) {
    
            Date a1 = new Date(2025, 3, 15);
            Date a2 = new Date(2025, 3, 15);
    
            System.out.println(a1 == a2); // false
            System.out.println(a1.equals(a2)); // false
        }
    }
  • 这种默认行为适用于需要严格判断对象唯一性的场景,但无法满足基于对象内容比较的需求

重写 equals() 的必要性

当需要比较对象的内容而非内存地址时,子类需重写 equals() 方法。例如,String 类重写了 equals(),使其比较字符串的字符序列是否相同。重写 equals() 必须遵循以下规范:

  • 自反性x.equals(x) 必须为 true
  • 对称性:若 x.equals(y) 为 true,则 y.equals(x) 也需为 true
  • 传递性:若 x.equals(y) 和 y.equals(z) 均为 true,则 x.equals(z) 也需为 true
  • 一致性:多次调用 equals() 的结果应一致(除非对象被修改)。
  • 非空性x.equals(null) 必须返回 false

下面重写Date里的equals,比较两个日期是否相等:

    @Override
    public boolean equals(Object obj){
        if(obj == null) return false; // 若obj为null,则一定不相等
        if(obj == this) return true; // 若两个对象的地址一样,则一定相等
        if(obj instanceof Date){
            Date d = (Date) obj;
            return year == d.year && month == d.month && day == d.day;
        }
        return false;
    }
        System.out.println(a1 == a2); //false
        System.out.println(a1.equals(a2)); //ture

 当我们比较两个字符串时:

    public static void main(String[] args) {

        String a1 = new String("hello");
        String a2 = new String("hello");

        System.out.println(a1 == a2); // false
        System.out.println(a1.equals(a2)); // true
    }

此时源码:

    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);
    }
    @IntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

 当我们直接输出a1时,直接输出了hello

        System.out.println(a1);

这是因为在String里toString被重写了 

    public String toString() {
        return this;
    }

hashCode()

在 Java 中,hashCode()是 Object`类定义的方法,用于返回对象的哈希码值。其核心作用是为对象提供一种快速计算的整数标识,常用于哈希表(如 HashMap、HashSet)等数据结构中,以提高存储和查询效率。

源码:

    @IntrinsicCandidate
    public native int hashCode();

1. hashCode() 的作用与特性
   哈希算法的本质
     hashCode() 的实现基于哈希算法,其核心特性是:  
     - 相同输入必得相同输出:同一对象多次调用 hashCode() 必须返回相同值(前提是对象未被修改)。  

    public static void main(String[] args) {

        String a1 = new String("hello");
        String a2 = new String("hello");

        System.out.println(a1.hashCode()); // 99162322
        System.out.println(a2.hashCode()); // 99162322
    }

    - 不同输入大概率不同输出:不同对象应尽量生成不同的哈希码,以减少哈希碰撞(即不同对象哈希码相同的情况)。  

    public static void main(String[] args) {

        String a1 = new String("hello");
        String a2 = new String("Hello");

        System.out.println(a1.hashCode()); // 99162322
        System.out.println(a2.hashCode()); // 69609650
    }

2. hashCode() 与 equals() 的关系
   - 强制规则
     若两个对象通过 equals() 判断为相等,则它们的hashCode() 必须返回相同值。反之,哈希码相同不代表对象相等(可能发生哈希碰撞)。  
   - 重写要求
     当自定义类重写 equals()时,必须同步重写 hashCode(),否则会导致基于哈希的集合类(如 HashMap)行为异常。

3. 哈希碰撞与设计原则
   - 碰撞的定义
         哈希碰撞指不同对象生成相同的哈希码。
   - 减少碰撞的策略
        设计哈希算法时需尽量均匀分布哈希值。  
        对于自定义类,可通过组合关键字段的哈希码(如 Objects.hash(field1, field2))来优化。

4. 实际应用场景
   - 哈希表的高效性 
     HashMap 通过 hashCode() 快速定位桶(Bucket),再通过 `equals()` 精确匹配键值。若哈希码分布不均,会导致链表或红黑树过长,降低性能。  
   - 数据完整性验证
     哈希算法(如 `hashCode()`)可用于校验数据是否被篡改。例如,对比文件的哈希值与原始值是否一致。  
   - 安全风险
     若直接使用 `hashCode()` 存储敏感信息(如用户密码),可能因哈希碰撞或算法弱点导致安全漏洞。实际开发中应使用加密哈希算法(如 SHA-256)而非 `hashCode()`。

 5. 注意事项与最佳实践
   - 避免依赖默认实现
     Object 类的默认 hashCode() 基于内存地址生成,不适用于需要逻辑相等性的场景。  
   - 缓存哈希码
     对于不可变对象(如 String),可在首次计算后缓存哈希码以提升性能。  
   - 工具类的使用
     使用 `Objects.hash()` 或 Apache Commons 的 HashCodeBuilder 简化哈希码生成。

  总结
`hashCode()` 是 Java 中实现高效数据存储与检索的核心机制,其设计需遵循与 `equals()` 的一致性规则,并注重减少哈希碰撞。在实际开发中,合理重写 `hashCode()` 能显著提升哈希表性能,而误用则可能导致逻辑错误或安全风险。

clone()

clone() 用于创建并返回当前对象的副本。其核心目的是实现对象的复制,但因其设计复杂性和潜在风险,需谨慎使用。

我们创建一个User类,有name,age,我们在Text里面测试一下

public class Text {
    public static void main(String[] args) {
        User u1 = new User("John", 25);

        u1.clone(); // 报错
    }
}

看源码:

    @IntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

clone()是被protected修饰的,只能在同包的本类、子类下使用。(native表示本地代码,并非Java代码本身)

Object 类中的 clone() 方法默认是 protected,因此直接通过对象调用 clone()(如 u1.clone())会因访问权限不足导致编译错误,除非 User 类重写并公开了 clone() 方法。

这里我们进行重写、公开并抛出异常 :

    @Override
    public Object clone() throws CloneNotSupportedException { //定义为public方法,我们可以在任何地方调用
        return super.clone();
    }
public class Text {
    public static void main(String[] args) throws CloneNotSupportedException {

        User u1 = new User("John", 25);

        Object o = u1.clone();
    }
}

此时会出现异常CloneNotSupportedException,因为当我们想要克隆一个对象,就必须实现克隆接口 public interface Cloneable { } ,这是一个标记接口,JVM会识别他,表示该类可被克隆

public class User implements Cloneable

此时

        User u1 = new User("John", 25);
        System.out.println(u1); // oop.User.User@2f4d3709
        Object o = u1.clone();
        System.out.println(o); // oop.User.User@4e50df2e

当我们重写toSpring后

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
        User u1 = new User("John", 25);
        System.out.println(u1); // User [name='John', age=25]
        Object o = u1.clone();
        System.out.println(o); // User [name='John', age=25]

 当我们修改克隆对象时,原对象不会被修改

        // 修改克隆对象(浅拷贝)
        User u2 = (User) o;
        u2.setName("Mary");
        u2.setAge(30);
        System.out.println(u2); // User [name='Mary', age=30]
        System.out.println(u1); // User [name='John', age=25]

那我们继续看下面这个问题:

public class Address {

    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    public Address() {}

    @Override
    public String toString() {
        return "Address{" +
                "city='" + city + '\'' +
                ", street='" + street + '\'' +
                '}';
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public void setStreet(String street) {
        this.street = street;
    }
}
public class User implements Cloneable {
    private String name;
    private int age;
    private Address addr;

    public User(String name, int age,Address addr) {
        this.name = name;
        this.age = age;
        this.addr = addr;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

    public void setAge(int age) {
        this.age = age;
    }

    public Address getAddr() {
        return addr;
    }

    public void setAddr(Address addr) {
        this.addr = addr;
    }


    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", addr=" + addr +
                '}';
    }

    @Override
    public Object clone() throws CloneNotSupportedException { //定义为public方法,我们可以在任何地方调用
        return super.clone();
    }
}
public class Text {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address a1 = new Address("北京", "海淀区");
        User u1 = new User("John", 25,a1);

        User u2 = (User) u1.clone();
        u2.setName("Mary");
        u2.getAddr().setCity("上海");

        System.out.println(u1);
        System.out.println(u2);
    }
}

这样子就将原对象的地址也改了,因为我们使用的是浅拷贝

浅拷贝(Shallow Copy)
仅复制对象的顶层属性。若属性是基础数据类型(如 numberstring),直接复制值;若属性是引用数据类型(如对象、数组),则复制其内存地址,新旧对象共享同一块堆内存。 

深拷贝(Deep Copy)
递归复制对象的所有层级,包括引用类型数据,新旧对象完全独立,修改互不影响。

我们来使用深拷贝

    @Override
    public Object clone() throws CloneNotSupportedException {
        Address copyAddr = (Address) this.getAddr().clone();
        User copyUser = (User)super.clone();
        copyUser.setAddr(copyAddr);
        return copyUser;
    }

因为上面要克隆Address,所以我们要重写Address里的clone(记得impements Cloneable) 

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

这样我们就可以成功修改克隆对象了,下面是浅拷贝内存图(来源:动力节点)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值