软件构造 | Equality in ADT and OOP
🧇1 Three ways to regard equality
1.1 Using AF to define the equality
ADT是对数据的抽象, 体现为一组对数据的操作
抽象函数AF:内部表示→抽象表示
基于抽象函数AF定义ADT的等价操作,如果AF映射到同样的结果,则等价
1.2 Using observation to define the equality
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这 两个对象是等价的。
1.3 == vs. equals()
== 引用等价性 : 对基本数据类型,使用
==
判定相等equals() 对象等价性 : 对对象类型,使用equals()
在自定义ADT时,需要根据对“等价”的要求, 决定是否重写Object的equals()
1.4 Implementing equals()
The equals() method is defined by Object , and its default implementation looks like this:
Note:在Object中实现的默认equals()是在判断引用等价性,这通常不是程序员所期望的,因此,需要重写。
public classDuration {
...
// Problematic definition of equals()
public boolean equals(Duration that) {
return this.getLength() == that.getLength();
}
}
错误声明equal:实现的是重载而不是重写。
正确声明
在 Java 中,equals()
方法是用于比较两个对象是否相等的方法。在 Java 中,所有的类都继承自 Object
类,而 Object
类中的 equals()
方法默认实现是比较两个对象的引用是否相同(即比较内存地址)。因此,当需要在自定义类中比较对象内容时,通常需要重写 equals()
方法。
下面是一个示例,展示了如何重写 equals()
方法来比较自定义类中的对象内容:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true; // 如果是同一个对象,直接返回true
}
if (obj == null || getClass() != obj.getClass()) {
return false; // 如果对象为null或者是不同类的实例,直接返回false
}
Person person = (Person) obj; // 强制转换为Person类
return age == person.age && name.equals(person.name); // 比较name和age是否相等
}
// 省略 getter 和 setter 方法
}
public class Example {
public static void main(String[] args) {
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
System.out.println(person1.equals(person2)); // 输出true,因为内容相同
}
}
在上面的示例中,Person
类重写了 equals()
方法,根据对象的 name
和 age
属性来比较两个 Person
对象是否相等。在 equals()
方法中,首先比较对象引用是否相同,然后再比较类和属性是否相同。
需要注意的是,在重写 equals()
方法时,通常需要同时重写 hashCode()
方法,以确保两个方法的一致性。这样可以保证对象在放入基于散列的集合(如 HashMap
、HashSet
)时能够正确地根据对象内容对其进行存储和检索。
🍕 2The Object contract(对象合同)
- 等价关系:自反、传递、对称。
- 除非对象被修改了,否则调用多次equals应是同样的结果。
- “相等”的对象,其hashCode()的结果必须一 致。
Note:用“是否为等价关系”检验你的equals()是否正确
2.1 Hash Tables
Object ’s default hashCode() implementation is consistent with its default equals()
键值对中的 key被映射为hashcode,对应到数组的index, hashcode决定了数据被存 储到数组的哪个位置
2.2 Overriding hashCode()
在 Java 中,每个对象都有一个 hashCode
,它是对象的哈希码,用于确定对象在哈希表等数据结构中的存储位置。hashCode
的作用是为了更高效地进行对象的存储和检索,特别是在使用基于散列的集合(如 HashMap
、HashSet
)时非常重要。
hashCode
方法的设计要求是:
- 如果两个对象通过
equals()
方法相等,则它们的hashCode
必须相等。 - 如果两个对象的
hashCode
相等,它们并不一定通过equals()
方法相等。
在实现类中重写 hashCode
方法时,通常需要保证满足上述两个要求。通常情况下,可以利用对象的属性来生成 hashCode
值,这样可以确保同样属性的对象具有相同的 hashCode
。
最简单方法:让所有对象的
hashCode
为同一 常量,符合contract
,但降低了hashTable
效率通过
equals
计算中用到的所有信息的hashCode
组合出新的hashCode
在 Java 中,当两个对象通过 equals()
方法相等且具有相同的 hashCode
值时,如果将这两个对象放入基于散列的集合(如 HashMap
、HashSet
)中,只会存储一个对象。这是因为散列集合在存储对象时会先根据 hashCode
值确定对象在内部数据结构中的存储位置,然后再通过 equals()
方法来判断具体位置是否已经存在相同的对象。
具体来说,当向散列集合中添加一个对象时,首先会计算该对象的 hashCode
值,然后根据 hashCode
值找到对象在内部存储结构中的位置。如果在该位置处已经有一个对象存在,并且这个对象与新添加的对象通过 equals()
方法比较相等(即返回 true
),那么新添加的对象不会被存储,以保证集合中不会存在重复的对象。
下面是一个简单示例,演示了两个相等的对象具有相同 hashCode
值时,只存储一个对象的情况:
import java.util.HashMap;
import java.util.Map;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
return this.name.equals(other.name) && this.age == other.age;
}
}
public class Example {
public static void main(String[] args) {
Map<Person, String> personMap = new HashMap<>();
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
personMap.put(person1, "Value 1");
personMap.put(person2, "Value 2");
System.out.println(personMap.size()); // 输出 1,因为两个对象相等且具有相同的 hashCode
}
}
在上面的示例中,Person
类重写了 equals()
和 hashCode()
方法,确保在两个对象具有相同属性值时返回 true
并且具有相同的哈希码。当将这两个相等的对象放入 HashMap
中时,只会存储一个对象,因为它们具有相同的 hashCode
值。因此,最终输出的大小是 1
。
Always override hashCode() when you override equals()
除非你能保证你的ADT不会被放入到Hash类型的集合类中
🍿 3引用的概念
在 Java 中,引用是指向对象的指针或句柄。在 Java 中,所有对象都是通过引用来操作的,而不是直接访问对象本身。当您创建一个对象时,实际上是在堆内存中为该对象分配了空间,并返回一个引用,这个引用指向堆中的对象。Java 的引用是一种高级抽象概念,开发人员无法直接控制对象所在的内存位置,只能通过引用去访问和操作对象。
与此相对应,C 语言中的指针是直接指向内存地址的变量。在 C 中,通过指针可以直接访问或修改内存地址中的数据。指针在 C 语言中被广泛用于实现动态内存分配、访问数组元素、操作数据结构等。
下面是一个简单的示例来对比 Java 中的引用和 C 中的指针:
在 Java 中:
public class Example {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = str1;
System.out.println(str1); // Hello
System.out.println(str2); // Hello
str2 = "World";
System.out.println(str1); // Hello
System.out.println(str2); // World
}
}
在这个示例中,str1
和 str2
都是对象的引用,它们最初都指向同一个字符串对象"Hello"。当我们修改 str2
的值时,它指向了一个新的字符串对象"World",但str1
仍然指向原来的字符串对象"Hello"。
在 C 中:
#include <stdio.h>
int main() {
int var = 10;
int* ptr = &var;
printf("Original value: %d\n", var); // 10
printf("Value through pointer: %d\n", *ptr); // 10
*ptr = 20;
printf("Updated value: %d\n", var); // 20
printf("Value through pointer: %d\n", *ptr); // 20
return 0;
}
在这个示例中,ptr
是一个指向 var
变量的指针。通过 *ptr
可以访问或修改 var
的值。在这段代码中,我们通过指针修改了 var
的值,而不是通过变量名 var
直接修改。
综上所述,Java 中的引用是指向对象的抽象概念,开发者无法直接操作对象的内存地址,只能通过引用访问对象;而 C 中的指针直接指向内存地址,开发者可以直接控制和操作内存地址中的数据。