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)
仅复制对象的顶层属性。若属性是基础数据类型(如 number
、string
),直接复制值;若属性是引用数据类型(如对象、数组),则复制其内存地址,新旧对象共享同一块堆内存。
深拷贝(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();
}
这样我们就可以成功修改克隆对象了,下面是浅拷贝内存图(来源:动力节点)