目录
引
前面的OOP部分都在讲类,这篇整理了一下对 对象本身的操作。虽然自己还没对象,想new一个(bushi)
对象比较
”引用比较“与“内容比较”
▪ 对于基本数据类型的变量,可以使用“==”,“>”和“<”进行判等和大小比较,但除了“==”之外,“>”和“<”是不能直接用于引用类型的变量的。
▪ 经过前面的学习,我们己经知道,施加于两个对象变量之上的“==”,实际上是判断这两个对象变量是否引用同一个对象。
▪ 在实际开发中,我们经常需要比对某两个对象的“内容” ,比如你己经有了一个对象,想在另一个对象集合中找到是否有“内容一样”的对象。
▪ 这里所说的对象的“内容”,主要指对象的字段值。
▪ 由于字段值在不同的时刻可能会改变,在某一特定时刻,对象所有字段值的集合,称为对象在这一时刻的“状态”。
对象的比较:Comparable接口
JDK中为了能比较两个对象的大小,定义了以下接口:
public interface Comparable {
int compareTo(Object other);
}
compareTo()方法的返回值,约定如下:
1. 对象X和Y相等,返回“0”
2. 对象X<Y,返回“-1”
3. 对象X>Y,返回“1”
泛型化的Comparable接口
引入泛型特性后,JDK中又增加了一个泛型化的接口,老的版本现在已经不再推荐使用了:
public inteface Comparable<T> {
int compareTo(T other);
}
使用例子
import java.util.*;
public class StudentSortTest {
public static void main(String[] args) {
Student[] staff = new Student[3];
staff[0] = new Student("张三", 25);
staff[1] = new Student("李四", 20);
staff[2] = new Student("王五", 10);
//排序
Arrays.sort(staff);
for (Student e : staff)
System.out.println("姓名=" + e.getName() + "; 年龄=" + e.getage());
}
}
class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String n, int s) {
name = n;
age = s;
}
public String getName() {
return name;
}
public double getage() {
return age;
}
public int compareTo(Student other) {
if (age < other.age) return -1;
if (age > other.age) return 1;
return 0;
}
}
这个例子中手动实现了compareTo()方法。
Arrays.sort()方法会调用每个Student对象的compareTo()方法,以确定元素在数组中的顺序。
通过compareTo()方法,Array.sort() 会对数组中的两个元素两两比较,从而对staff数组进行排序。
这和c++中的sort自己写一个cmp函数是一个道理。
▪ JDK中凡是支持大小比较的类型(比如Integer),都实现了Comparable<T> 接口。
Integer 的源码:
“==”与“equals”
之前的文章中提到过,“==”施加于引用类型变量,是比较两个对象变量是否引用同一对象。如果需要比对对象的“内容(即各字段的值)”,通常是调用对象的equals方法。
▪ equals方法由Object类所定义,其默认实现如下:
public boolean equals(Object obj) {
//默认情况下是比较对象引用,而不是对象“内容”
return (this == obj);
}
子类可根据实际情况,“重写(Override)”Object类的equals方法。
重写equals方法,其实就是要你确定一下“两个对象怎样才算相等”。根据自己的具体需要来写。
class MyClass {
public int InnerValue;
public String InnerString;
public boolean equals(Object obj) {
boolean result = obj instanceof MyClass;
if (!result) {
return false;
} else {
MyClass other = (MyClass) obj;
return (other.InnerValue == this.InnerValue)
&& (other.InnerString.equals(this.InnerString));
}
}
}
public class IsTwoObjectEquals {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.InnerString = "Hello";
obj.InnerValue = 100;
MyClass other = new MyClass();
other.InnerString = "Hello";
other.InnerValue = 100;
System.out.println(obj.equals(other));
}
}
就比如说这个代码中,就重写为:InnerValue相等时,两个对象相等。
重写equals()的必要性
▪ JDK的许多集合类型,比如ArrayList,在查找元素时,会调用元素的equals()方法以确定当前元素是不是要找的那个。
如果你写了一个类,把对象放到ArraryList集合中,那么,你需要重写equals方法,才能在集合中使用indexOf()方法查找到特定的对象,这个就是为什么你在定义类时,需要重写equals()方法的原因。
重写equals方法的要求
▪ 自反性(reflexive): x.equals(x)= true
▪ 对称性(symmetric):如果 x.equals(y) =true,那么 y.equals(x)=true
▪ 传递性(transitive):如果 x.equals(y) = true 并且y.equals(z) = true,那么x.equals(z)= true. (回想起离散数学的知识了)
▪ 一致性(consistent):只要对象的状态没有发生改变, x.equals(y)的多次调用应该返回一致的结果
▪ x.equals(null) = false
▪ equals()方法要与compareTo()方法的返回值一致。当equals()方法返回true时,compareTo()方法应该返回0。所以,为了保持两个方法结果的一致性,可以在重写equals()方法时,直接调用compareTo()方法,以避免相同的对象比较规则重复写两遍。
▪ 要重写Object的equals方法,注意其参数类型必须是“Object”,如果是其它类型,就是“重载(overload)”,导致该方法不会覆盖Object
类的equals()
方法。
public boolean equals(Object obj) { //方法签名必须这样写才是重写
//...
}
重写hashCode( )方法
▪ 另外,为了让对象能放入JDK所提供的各种集合中,通常还需要重写hashCode()方法,因为这些集合在内部可能会需要依据对象的hashCode值进行定位。
- 在基于哈希的集合(如
HashSet
、HashMap
等)中,查找和存储对象时首先根据对象的hashCode
值定位到存储桶(bucket)或区域,然后再使用equals()
进行精确匹配。如果没有正确重写hashCode()
方法,那么即使两个对象根据equals()
相等,它们的hashCode
值可能也不相同,导致集合无法找到目标对象。
hashCode()
与 equals()
的关系
- 相等性约定:如果
equals()
判断两个对象相等,那么它们的hashCode()
值必须相同。 - 反之并不要求:如果两个对象的
hashCode()
值相同,equals()
不一定返回true
,这是因为hashCode()
值相同只是意味着它们可能在同一个存储区域,具体相等性还需equals()
来判断。
重写 hashCode()
的规则
重写hashCode()
时可以基于对象的属性生成哈希值,确保当equals()
判断两个对象相等时,它们的hashCode()
也相等。例如:
@Override
public int hashCode() {
return Objects.hash(age); // 使用属性生成哈希值
}
Objects.hash( )
是Java提供的一个方法,它可以根据传入的属性自动生成哈希值,并确保符合hashCode
和equals
的一致性要求。
- 必须同时重写
equals()
和hashCode()
,确保对象在集合中基于逻辑相等性进行查找和存储。 equals()
定义逻辑相等性,hashCode()
提供有效的散列支持。- 若只重写
equals()
或hashCode()
,会导致在集合中存取对象时出现异常行为或无法查找的情况。
对象组合
对象组合指的是对象之间的相互包容关系。
在面向对象的语言中:
对象的两种组合方式
▪ 一个对象包容另一个对象,称为“对象组合”。
合成(Composition)
合成是更强的“整体-部分”关系,意味着部分对象的生命周期依赖于整体对象。
▪ A完全包容B。
▪ A对象创建时,B对象自动创建,同样地,A对象销毁时,B对象也同时销毁。
例如,一个House
类由多个Room
组成,但Room
不能脱离House
单独存在。如果销毁House
,则Room
也会随之销毁。
- 特性:合成关系中“部分”对象依赖于“整体”对象的生命周期。
- 实现:在Java中,通过在“整体”对象构造和销毁时创建或销毁“部分”对象来实现。
class House {
private Room room; // 合成关系
public House() {
room = new Room(); // 创建House时创建Room
}
public Room getRoom() {
return room;
}
}
class Room {
// Room的生命周期依赖House
}
在此例中,Room
的生命周期与House
一致,当House
销毁时,Room
也随之销毁。
关联(Association)
关联指两个对象之间有某种连接或依赖关系,但彼此可以独立存在。例如,学生和课程之间的关系:学生参加某些课程,课程也可以包含多个学生,但二者在生命周期上是独立的。
- 特性:对象在关系中互相引用但仍可以独立存在。
- 实现:在Java中,可以通过字段引用来实现关联。比如,
Student
类中包含Course
的引用,表示学生与课程的关联关系
class Student {
private String name;
private Course course; // 学生关联到某个课程
public Student(String name, Course course) {
this.name = name;
this.course = course;
}
public String getCourseName() {
return course.getCourseName();
}
}
class Course {
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
}
在此例中,Student
类和Course
类通过course
字段相互关联,但二者独立存在,可以相互替换或修改,而不会影响对方。
实际使用
比如,在桌面应用中,窗体与窗体上各种控件之间的关系,就是第一种对象组合方式,窗体对象负责管理控件对象的生命周期,当窗体对象销毁时,所有控件对象也随之销毁。
JDK中的许多集合,比如ArrayList<T>,它与放在集合中的对象之间,就是第二种对象组合方式,集合对象自己,与放在集合中的对象,其生命周期是相互独立的。
对象复制
▪ 所谓“对象复制”,是指这样的一种情景:我己经有一个对象A了,我希望把它克隆N份,得到N个与A内容一模一样的对象。
浅拷贝
先举一个简单的例子:
package shallow;
class A {
public int i = 100;
}
public class ShallowCopy {
public static void main(String[] args) {
A a = new A();
//开始克隆
A other = CloneObject(a);
System.out.println(a == other);
}
static A CloneObject(A obj) {
A newObj = new A();
newObj.i = obj.i;
return newObj;
}
}
前面的示例代码中,对象的复制其实如下图所示:
像这样,把一个对象的所有字段值,逐个地复制到另一个对象的对应字段,这种对象复制方式称为“浅拷贝(shallow copy)”。
对于组合对象,浅拷贝方式会带来一些问题。(研究过py创建二维列表的应该会有印象)
以下示例代码,注意到A对象包容一个B对象:
package shallow;
class A2 {
public int i = 100;
public B2 b; //A包容一个B的对象
public A2() {
b = new B2(); //创建被包容对象
}
}
class B2 {
public int j = 200;
}
public class ShallowCopy2 {
public static void main(String[] args) {
A2 a = new A2();
A2 other = CloneObject(a); //克隆对象
System.out.println(a==other);
System.out.println(a.b==other.b);
}
static A2 CloneObject(A2 obj) {
A2 newObj = new A2();
newObj.i = obj.i;
newObj.b = obj.b; //用于完成空对象工作的方法
return newObj;
}
}
在这个例子中,对象B没有被复制,而是被克隆前后的两个对象所“共享”,这并不符合“对象复制”的本意。我们希望完成的是“对象克隆”,即得到两个“一模一样的”,并且是“完全独立”的对象。
所以为了真正实现复制对象的功能,就需要进行深拷贝。
深拷贝
手动拷贝
我们可以手动实现深拷贝需要个功能,就是把字段一个个复制过去。
package deep;
class A {
public int i = 100;
public B b; //A包容一个B的对象
public A() {
b = new B(); //创建被包容对象
}
}
class B {
public int j = 200;
}
public class DeepCopy {
public static void main(String[] args) {
A a = new A();
A other = cloneObject(a);
System.out.println(a == other);
System.out.println(a.b == other.b);
}
static A cloneObject(A obj) {
A newObj = new A();
newObj.i = obj.i;
//创建一个被包容的内部对象
newObj.b = new B();
newObj.b.j = obj.b.j;
return newObj;
}
}
Cloneable
接口:递归调用clone()
方法
JDK中提供了一个Cloneable接口,需要实现深复制的对象应该实现这一接口。
Cloneable
接口本身并没有定义任何方法,它的作用只是标记对象是可克隆的。真正执行克隆的是Object
类的clone()
方法,但我们通常需要覆盖该方法以实现深拷贝。(像这种根本就没有定义任何一个成员的接口,称为“标记接口”。)
Object类提供了一个protected clone()方法,子类可将其定义为public的,从而向外界提供克隆自己的功能:依据JDK文档,一个对象选择实现Cloneable接口,必须重写Object类的clone方法,并把它改写为public的。
class DeepCopyDemoClass implements Cloneable {
public int i = 100;
public InnerClass b; //A包容一个B的对象
public DeepCopyDemoClass() {
b = new InnerClass(); //创建被包容对象
}
//重写基类的clone方法
public Object clone(){
var newObj = new DeepCopyDemoClass();
newObj.i = this.i;
newObj.b = new InnerClass();
newObj.b.j=this.b.j;
return newObj;
}
}
class InnerClass {
public int j = 200;
}
public class DeepCopy2 {
public static void main(String[] args) {
var a = new DeepCopyDemoClass();
var other = (DeepCopyDemoClass)a.clone();
System.out.println(a == other);
System.out.println(a.b == other.b);
}
}
当我们希望实现一个对象的深拷贝时,递归地调用clone()
方法就是指,如果对象包含了其他引用类型的对象,这些对象本身也需要实现Cloneable
接口,并覆盖它们的clone()
方法。这样每个引用类型对象都可以生成自己独立的副本。
基本步骤
- 实现
Cloneable
接口:在需要克隆的类上实现Cloneable
接口,表明该类是可克隆的。 - 覆盖
clone()
方法:覆盖类的clone()
方法,调用super.clone()
来创建当前对象的浅拷贝。 - 递归克隆引用字段:对于每个引用字段,调用它们的
clone()
方法,这样就实现了深拷贝。 - 异常处理:
clone()
方法要求处理CloneNotSupportedException
异常。
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 调用Object的浅拷贝
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 1. 首先浅拷贝Person对象自身
Person clonedPerson = (Person) super.clone();
// 2. 调用address字段的clone()方法,实现深拷贝
clonedPerson.address = (Address) address.clone();
return clonedPerson;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("New York");
Person p1 = new Person("Alice", address);
// 深拷贝Person对象
Person p2 = (Person) p1.clone();
System.out.println(p1.address.city); // 输出: New York
System.out.println(p2.address.city); // 输出: New York
// 修改p1的address对象不会影响p2的address对象
p1.address.city = "Los Angeles";
System.out.println(p1.address.city); // 输出: Los Angeles
System.out.println(p2.address.city); // 输出: New York
}
}
代码说明
-
Address
类:Address
实现了Cloneable
接口,并重写了clone()
方法。- 在
clone()
中调用super.clone()
来创建当前对象的浅拷贝(因为Address
没有复杂的引用类型属性,这样的浅拷贝已足够)。
-
Person
类:Person
也实现了Cloneable
接口并重写了clone()
方法。- 在
Person
的clone()
方法中,首先调用super.clone()
创建对象的浅拷贝。 - 然后,对包含的引用类型
address
字段也调用其clone()
方法,从而实现对address
对象的深拷贝。
-
测试效果:
- 调用
p1.clone()
生成p2
后,p1
和p2
对象中的address
字段不再指向同一个引用,确保修改p1.address
的内容不会影响p2.address
。
- 调用
如果一个类中包含多个嵌套的引用对象,例如Person
包含Address
对象,Address
还包含City
对象,则我们需要递归地在每一个嵌套的引用对象上实现clone()
方法。每一个引用对象都要实现Cloneable
接口并重写clone()
方法,这样递归的调用会实现整个对象树的深拷贝。