本文代码实践基于 JDK-17:
java 17.0.11 2024-04-16 LTS
书接上回《Effective Java:01 创建和销毁对象》,让我们继续学习。
10 覆盖 equals 时请遵守通用约定
Object
类中已经给出了 equals
方法的默认实现,如下所示:
public boolean equals(Object obj) {
return (this == obj);
}
这个默认实现所表达的意思是:类的每个实例都只与它自身相等,即对象(引用)相等。在单例模式中,对象实例就是天生相等的,因为就一个实例。而在实际编程中,我们很多时候还要关注逻辑相等,也就是值相等。
Book book1 = new Book("Effective Java", "Joshua Bloch");
Book book2 = new Book("Effective Java", "Joshua Bloch");
比如上面的 book1
和 book2
两个对象实例,虽然在操作系统中它们拥有不同的内存地址,但它们都代表同一本书,即我们日常中理解的书名和作者名都相同。 这就是逻辑相等。为了实现逻辑相等,就必须覆盖 Object
类中的默认实现,此时我们需要遵循一些通用约定:
1)自反性(Reflexivity)
对于任何非
null
的引用值x
,x.equals(x)
必须返回 true。
这一条约定说的是,对象必须等于其自身,所以无需过多赘述。
2)对称性(Symmetry)
对于任何非
null
的引用值x
和y
,当且仅当y.equals(x)
返回 true 时,x.equals(y)
必须返回 true。
第二条说的是,任何两个对象对于“它们是否相等”的问题都必须保持一致。同一个类进行比较时,一般不会有这个问题,除非是两种不同的类对象,比如书中给出的 java.lang.String
和自定义 CaseInsensitiveString
的例子,让 CaseInsensitiveString
兼容比较 String
,但反过来则不行:
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if (o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(s);
}
}
在 JDK 中,java.sql.Timestamp
及其父类 java.util.Date
就是对称性的反面教材。对此,Timestamp
类中还给出了相关说明,比如:
Note: ......As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.
Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.
3)传递性(Transitivity)
对于任何非
null
的引用值x
、y
和z
,如果x.equals(y)
返回 true,且y.equals(z)
也返回 true,那么x.equals(z)
也必须返回 true。
同样,对于一般情况来说,这个约定也不会被打破。但如果出现子类、父类或兄弟类之间的混合比较,则可能会出现传递性被破坏的情况。书中使用各种方式想方设法来达到父子兄弟类的 equals
传递性,但最终发现不可行:我们无法在扩展可实例化的类的同时,既增加新的值组件(子类中有新的属性字段,并且用于比较),同时又保留 equals
约定,除非放弃面向对象抽象所带来的优势。
如果想避免上述问题,一种方式是使用抽象父类,并且父类中没有任何值组件;第二种方式则是使用组合,将原先的父类作为当前类的一个属性,此时进行 equals
比较就跟常规类别无二致了。
4)一致性(Consistency)
对于任何非
null
的引用值x
和y
,只要比较使用的对象信息没有被修改,x.equals(y)
的值不会随着时间的改变而改变。
也就是说,如果对象被改变了,那么 x.equals(y)
的返回值很可能也会被改变。但不可变对象则没有这个问题:相等的对象永远相等,不想等的对象永远不相等。一个值得记住的原则是:不要让 equals
方法依赖不可靠的资源。
java.net.URL
就是一致性的反面教材:
/**
* ...
*
* Two URL objects are equal if they have the same protocol, reference
* equivalent hosts, have the same port number on the host, and the same
* file and fragment of the file.<p>
*
* Two hosts are considered equivalent if both host names can be resolved
* into the same IP addresses; else if either host name can't be
* resolved, the host names must be equal without regard to case; or both
* host names equal to null.<p>
*
* Since hosts comparison requires name resolution, this operation is a
* blocking operation. <p>
*
* Note: The defined behavior for {@code equals} is known to be inconsistent
* with virtual hosting in HTTP.
*
* ...
*/
public boolean equals(Object obj) {
if (!(obj instanceof URL u2))
return false;
return handler.equals(this, u2);
}
因为 URL#equals
比较时,需要对主机 IP 进行比较,但主机 IP 又需要根据主机名通过网络解析,此时我们就无法保证随着时间的推移,原先的主机名所对应的 IP 会如何变化。所以,针对 equals
方法的计算,应该是确定性的计算。
5)非空性(Non-nullity)
对于任何非
null
的引用值x
,x.equals(null)
必须返回 false。
关于这一点,这里也不做额外补充。
说完几个约定后,一个高质量的 equals
方法该如何编写?请参考下面的例子:
@Override
public boolean equals(Object o) {
// 1. 使用 == 检查当前是同一个对象引用
if (this == o) {
return true;
}
// 2. 使用 instanceof 检查对象类型
if (!(o instanceof CaseInsensitiveString)) {
return false;
}
// 3. 把参数转换成正确的类型
CaseInsensitiveString that = (CaseInsensitiveString) o;
// 4. 依次检查对象的关键域,并从最有可能出现不一致的域开始比较,以提高性能
return s.equalsIgnoreCase(that.s);
}
最后,再额外补充几点:
- 非
float
或double
的基本类型,可以使用==
进行比较; - 针对
float
或doulbe
类型,请使用静态方法Float.compare(float, float)
或Double.compare(double, double)
进行比较。当然可以使用Float.equals(Object)
或Double.equals(Object)
,但存在自动装箱拆箱的性能开销; - 如果需要比较数组域中的每个元素,可以考虑使用
Arrays.equals
; - 不要将
equals
方法中的入参类型从Object
改为当前类的类型,此时就不是重写,而是重载。
11 覆盖 equals 总要覆盖 hashCode
为什么要有这项规定?一个典型案例是 HashMap
中 key 的计算会用到 hashCode,比如下面的代码示例:
class CodeDemo {
private final String name;
public CodeDemo(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CodeDemo that = (CodeDemo) o;
return Objects.equals(name, that.name);
}
// @Override
// public int hashCode() {
// return Objects.hash(name);
// }
@Override
public String toString() {
return "CodeDemo{" +
"name='" + name + '\'' +
'}';
}
}
@Test
void hashMapKeyTest() {
Map<CodeDemo, String> map = new HashMap<>();
map.put(new CodeDemo("Jack.Ma"), "111");
map.put(new CodeDemo("123"), "123");
log.info("demo: {}", map.get(new CodeDemo("Jack.Ma")));
log.info("demo2: {}", map.get(new CodeDemo("123")));
}
当我们将 CodeDemo
中的 hashCode()
方法注释掉以后,单元测试 hashMapKeyTest
返回的结果如下:
15:51:56.771 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo: null
15:51:56.772 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo2: null
之所以会出现这个问题,正是因为 HashMap
会计算对象的 hashCode:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// ...
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// ...
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// ...
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
// ...
}
由于没有重写 hashCode
方法,两次新创建的 CodeDemo
实例其 hashCode 是不相同的,我们可以使用下面的代码打印验证:
log.info("demo1.hashCode: {}", new CodeDemoEx("Jack.Ma").hashCode());
log.info("demo2.hashCode: {}", new CodeDemoEx("Jack.Ma").hashCode());
打印结果为:
16:11:43.243 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo1.hashCode: 1123226989
16:11:43.243 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo2.hashCode: 500885941
那么,如果重写 hashCode()
方法,一般如何计算当前类对象的 hashCode 值呢?其实不必慌张,各种 IDE 工具都能够自动生成,比如下面是 IDEA 中的界面:
当我们选一个参数或两个参数计算 hashCode 时,其方式是不一样的:
@Override
public int hashCode() {
return Objects.hashCode(name);
}
@Override
public int hashCode() {
return Objects.hash(name, value);
}
查看 Objects
类相关源码:
public final class Objects {
// ...
/**
* Returns the hash code of a non-{@code null} argument and 0 for
* a {@code null} argument.
*
* @param o an object
* @return the hash code of a non-{@code null} argument and 0 for
* a {@code null} argument
* @see Object#hashCode
*/
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
/**
* Generates a hash code for a sequence of input values. The hash
* code is generated as if all the input values were placed into an
* array, and that array were hashed by calling {@link
* Arrays#hashCode(Object[])}.
*
* <p>This method is useful for implementing {@link
* Object#hashCode()} on objects containing multiple fields. For
* example, if an object that has three fields, {@code x}, {@code
* y}, and {@code z}, one could write:
*
* <blockquote><pre>
* @Override public int hashCode() {
* return Objects.hash(x, y, z);
* }
* </pre></blockquote>
*
* <b>Warning: When a single object reference is supplied, the returned
* value does not equal the hash code of that object reference.</b> This
* value can be computed by calling {@link #hashCode(Object)}.
*
* @param values the values to be hashed
* @return a hash value of the sequence of input values
* @see Arrays#hashCode(Object[])
* @see List#hashCode
*/
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
// ...
}
public class Arrays {
// ...
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
// ...
}
可以看到,如果是计算一个变量,就直接返回对象自身的 hashCode,否则会根据下面的公式进行累加计算:
int result = result * 31 + c.hashCode
公式中的 c 指的是当前正在计算的变量,而 result 则指代已经累计的值。
为什么要乘以 31 ?主要是为了避免哈希冲突,习惯上都会使用一个奇素数来计算散列结果。另外,31 * i
可以被优化成移位和减法运算(即 (i << 5) - i
,从而提高性能,推导过程也很简单:
31 * i
= (32 -1) * i
= (2^5 -1) * i
= (i << 5) -i
但我们也需要了解到,上面的 Objects#hash
方法,其传参是一个数组,这会导致每次计算时,都会导致数组的创建以及对基本类型的装箱和拆箱,从而影响性能。所以,如果一个类是不可变的,并且计算 hashCode 的开销比较大,则可以考虑将 hashCode 缓存起来。另外,也不要将 hashCode 方法的返回值做出具体的规定,从而为后续的优化提供灵活性,像 String 和 Integer 中的做法(将 hashCode 方法返回的确切值规定为该实例值的一个函数)都不值得推荐:
public class Integer {
// ...
@Override
public int hashCode() {
return Integer.hashCode(value);
}
/**
* Returns a hash code for an {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for an {@code int} value.
*/
public static int hashCode(int value) {
return value;
}
// ...
}
最后,简单梳理一下 Object
类中关于 hashCode
方法的几条约定:
- 一个对象的
equals
方法所使用的比较字段如果没有变化,那么多次调用hashCode
方法,其值也不会变化; - 两个对象调用
equals
方法相等,那么它们的hashCode
方法的计算结果也要相等; - 两个对象调用
equals
方法不相等,并不要求它们的hashCode
也不相等,但不相等会提高散列表的性能。
P.S. 如果执意于哈希冲突的概率,可以考虑 Google Guava 库中的 com.google.common.hash.Hashing
类,以下是一个使用样例:
@Test
void googleGuavaGoodFastHash() {
String demo = "2100-王子奇.Jack.Ma";
long start1 = System.nanoTime();
int hashCode = Objects.hash(demo);
long end1 = System.nanoTime();
log.info("cost1: {}", end1 - start1);
log.info("demo hashCode: {}, {}", hashCode, demo.hashCode());
HashFunction hashFunction = Hashing.goodFastHash(64);
long start2 = System.nanoTime();
Hasher hasher = hashFunction.newHasher();
hasher.putString(demo, Charset.forName("UTF-8"));
long end2 = System.nanoTime();
HashCode hashCode2 = hasher.hash();
log.info("cost2: {}", end2 - start2);
log.info("demo fast hashCode: {}", hashCode2);
}
// 输出结果为:
// 17:33:23.415 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - cost1: 7041
// 17:33:23.417 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo hashCode: 1044429992, 1044429961
// 17:33:23.419 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - cost2: 536916
// 17:33:23.419 [Test worker] INFO jvm.tech.demonlee.tmp.HashCodeTests - demo fast hashCode: 5918972eea3b5cf6804998ce00261dbf
12 始终要覆盖 toString
看到这一条,心下一惊,因为我在开发过程中也没有做到。很多时候,我只在需要用的时候,才会为某个类生成 toString
方法,而且还是通过 IDE 工具提供的快捷方式自动生成。
// Object 类中 toString 方法的默认实现
/**
* ……
*
* In general, the {@code toString} method returns a string that
* "textually represents" this object. The result should
* be a concise but informative representation that is easy for a
* person to read.
* It is recommended that all subclasses override this method.
* The string output is not necessarily stable over time or across
* JVM invocations.
* ……
* ……
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
之所以要覆盖 Object#toString()
,主要是方便查看,以及便于代码出现错误时调试,一个具备可读性的字符串是可以自描述的。比如打印 PhoneNum
对象实例,是默认的 PhoneNum@xxx
还是 PhoneNum:021-12309903
更好呢?
正如上面 Java Doc 中所说:一个简洁但信息丰富的表示,即 The result should be a concise but informative representation that is easy for a person to read
。那什么是简洁但信息丰富的表示呢?
- 重写
toString
时,没有必要将所有域都放进去,只需要那些能表达信息的域即可; - 敏感信息的域(比如密码,身份信息等)则坚决不能输出;
- 考虑是否要指定格式,比如电话号码中使用
-
将区号和尾号分开,JDK 内部的BigDecimal
,BigInteger
等类都指定了输出格式。无论是否指定格式,都要在 Java Doc 中表明意图,而一旦指定了格式,就表示后续不会轻易改变; - 静态工具类,枚举类等没有必要重写
toString
方法,其内置的实现已经可以满足大部分需求; - 当一个抽象类中存放了很多共用域时,建议在该抽象类中实现一个通用的
toString
方法。
13 谨慎地覆盖 clone
看完这一小节内容,笔者觉得这一条的标题有些委婉,我们可以稍微激进一点:大多数情况下,没必要使用这个 clone
方法。
// Object 类中 clone 方法的定义:它是一个本地方法
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
首先,我们看看如何使用 clone
方法。按照常规的做法,我们可能直接在当前类中重写:
public class FooBar {
private String name;
private int score;
public FooBar(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "FooBar{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
@Override
public FooBar clone() {
try {
return (FooBar) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
class CloneTests {
@Test
void should_failed_clone() {
FooBar fooBar = new FooBar("Jack.马", 100);
log.info("foo: {}", fooBar);
FooBar fooBarClone = fooBar.clone();
log.info("fooBarClone: {}", fooBarClone);
}
}
但此时会抛出异常 CloneNotSupportedException
:
14:19:05.892 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - foo: FooBar{name='Jack.马', score=100}
java.lang.CloneNotSupportedException: jvm.tech.demonlee.tmp.FooBar
java.lang.RuntimeException: java.lang.CloneNotSupportedException: jvm.tech.demonlee.tmp.FooBar
at jvm.tech.demonlee.tmp.FooBar.clone(FooBar.java:30)
at jvm.tech.demonlee.tmp.CloneTests.should_failed_clone(CloneTests.java:17)
......
......
显然,这种方式不行,正确的打开方式是需要实现 Cloneable
接口:
public class FooBar implements Cloneable {
// 其他代码同上
}
继续运行单元测试,结果正常:
14:11:28.776 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - foo: FooBar{name='Jack.马', score=100}
14:11:28.778 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBarClone: FooBar{name='Jack.马', score=100}
查看 Cloneable
接口的源码:
/**
* A class implements the {@code Cloneable} interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* {@code Cloneable} interface results in the exception
* {@code CloneNotSupportedException} being thrown.
* <p>
* By convention, classes that implement this interface should override
* {@code Object.clone} (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the {@code clone} method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since 1.0
*/
public interface Cloneable {
}
可以看到,该接口中没有任何方法声明,并且源码中已经给出了相应的使用说明。显然,这种使用方式令人感觉有些多此一举。正常情况下,我们实现某个接口是为了添加某类功能,而这里却改变了超类中受保护方法的行为,所以该方式是不值得推荐的。
让我们继续看如下代码:
public class FooBar implements Cloneable {
private String name;
private final int score;
private List<String> values;
public FooBar(String name, int score, List<String> values) {
this.name = name;
this.score = score;
this.values = new ArrayList<>(values);
}
public void addValue(String value) {
values.add(value);
}
public void removeValue(String value) {
values.remove(value);
}
@Override
public String toString() {
return "FooBar{" +
"name='" + name + '\'' +
", score=" + score +
", values=" + values +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FooBar fooBar)) return false;
return score == fooBar.score && Objects.equals(name, fooBar.name) &&
Objects.equals(values, fooBar.values);
}
@Override
public int hashCode() {
return Objects.hash(name, score, values);
}
@Override
public FooBar clone() {
try {
return (FooBar) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
class CloneTests {
@Test
void should_clone() {
FooBar fooBar = new FooBar("Jack.马", 100, Arrays.asList("牛", "马", "狗"));
log.info("foo: {}", fooBar);
FooBar fooBarClone = fooBar.clone();
log.info("fooBarClone: {}", fooBarClone);
log.info("fooBar == fooBarClone: {}", fooBar == fooBarClone);
log.info("fooBar equals fooBarClone: {}", fooBar.equals(fooBarClone));
log.info("fooBar.class == fooBarClone.class: {}", fooBar.getClass() == fooBarClone.getClass());
fooBar.removeValue("马");
fooBarClone.addValue("鼠");
log.info("fooBar: {}", fooBar);
log.info("fooBar equals fooBarClone: {}", fooBar.equals(fooBarClone));
}
}
我们在 FooBar
上增加了集合类型的字段 values,并且使用 final
修饰 score 字段,同时重写了 equals
和 hashCode
方法,运行结果如下:
14:55:30.718 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - foo: FooBar{name='Jack.马', score=100, values=[牛, 马, 狗]}
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBarClone: FooBar{name='Jack.马', score=100, values=[牛, 马, 狗]}
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBar == fooBarClone: false
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBar equals fooBarClone: true
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBar.class == fooBarClone.class: true
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBar: FooBar{name='Jack.马', score=100, values=[牛, 狗, 鼠]}
14:55:30.721 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - fooBar equals fooBarClone: true
结合运行结果,简单总结一下 clone
方法的几个约定,对于任何对象 x:
1)x.clone() != x
;
2)x.clone().getClass() == x.getClass()
;
3)x.clone().equals(x) == true
(若没有重写 equals
方法,则返回 false);
4)如果 x 中的域引用了可变的对象,该域只会被浅复制 ;
5)final 修饰的域虽然不能被重复赋值,但 clone 是底层的直接复制,可以绕过该限制。
简而言之,所有实现了
Cloneable
接口的类都应该覆盖clone
方法,并且是公有的方法,它的返回类型为类本身。该方法应该先调用super.clone
方法,然后修正任何需要修正的域。
其实完全没有必要这么麻烦,一个更简洁明了的方案是使用拷贝构造器(copy constructor)或拷贝工厂(copy factory),比如:
public FooBar(FooBar fooBar) {
this.name = fooBar.name;
this.score = fooBar.score;
this.values = new ArrayList<>(fooBar.values);
}
public static FooBar newInstance(FooBar fooBar) {
// return new FooBar(fooBar);
return new FooBar(fooBar.name, fooBar.score, new ArrayList<>(fooBar.values));
}
这类做法的好处:
1)不依赖语言之外并且有风险的对象创建机制;
2)没有类型转换,没有异常检测机制;
3)可以很友好地处理 final 域字段;
4)不需要遵守非良好的使用规范;
5)……
所以,如果可以,笔者觉得完全可以忽略 clone
这个方法。但凡事都有两面,是否有例外呢?还真有,就是数组的拷贝:
@Test
void should_clone_array() {
String a[] = new String[]{"你好", "Hello"};
String a2[] = a.clone();
log.info("a2: {}", Arrays.stream(a2).toList());
a2[1] = "Hi";
log.info("a: {}", Arrays.stream(a).toList());
log.info("a2: {}", Arrays.stream(a2).toList());
int c[][] = new int[][]{
{0, 3, 2, 9},
{2, 9, 222, 333}
};
int c2[][] = c.clone();
log.info("c2: {}", Arrays.stream(c2).toList());
c2[1][1] = -29;
log.info("c2: {}", Arrays.stream(c2).toList());
log.info("c: {}", Arrays.stream(c).toList());
}
运行结果如下:
16:24:49.223 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - a2: [你好, Hello]
16:24:49.225 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - a: [你好, Hello]
16:24:49.225 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - a2: [你好, Hi]
16:24:49.225 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - c2: [[0, 3, 2, 9], [2, 9, 222, 333]]
16:24:49.225 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - c2: [[0, 3, 2, 9], [2, -29, 222, 333]]
16:24:49.225 [Test worker] INFO jvm.tech.demonlee.tmp.CloneTests - c: [[0, 3, 2, 9], [2, -29, 222, 333]]
可以看到,这个例外本身也有例外,那就是:数组本身的拷贝其实也是浅复制,但一维数组往往没有问题,如果是多维数组则需要谨慎使用了。
14 考虑实现 Comparable 接口
Comparable
接口有一个 compareTo
方法,但它与前面几个方法有些不同,因为它没有定义在 Object
这个基类中。
/**
* This interface imposes a total ordering on the objects of each class that
* implements it. This ordering is referred to as the class's <i>natural
* ordering</i>, and the class's {@code compareTo} method is referred to as
* its <i>natural comparison method</i>.<p>
*
* ......
* ......
*
* It is strongly recommended (though not required) that natural orderings be
* consistent with equals. This is so because sorted sets (and sorted maps)
* without explicit comparators behave "strangely" when they are used with
* elements (or keys) whose natural ordering is inconsistent with equals. In
* particular, such a sorted set (or sorted map) violates the general contract
* for set (or map), which is defined in terms of the {@code equals}
* method.<p>
*
* ......
*
* Virtually all Java core classes that implement {@code Comparable}
* have natural orderings that are consistent with equals. One
* exception is {@link java.math.BigDecimal}, whose {@linkplain
* java.math.BigDecimal#compareTo natural ordering} equates {@code
* BigDecimal} objects with equal numerical values and different
* representations (such as 4.0 and 4.00). For {@link
* java.math.BigDecimal#equals BigDecimal.equals()} to return true,
* the representation and numerical value of the two {@code
* BigDecimal} objects must be the same.<p>
*
* ......
* ......
*/
public interface Comparable<T> {
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* <p>The implementor must ensure {@link Integer#signum
* signum}{@code (x.compareTo(y)) == -signum(y.compareTo(x))} for
* all {@code x} and {@code y}. (This implies that {@code
* x.compareTo(y)} must throw an exception if and only if {@code
* y.compareTo(x)} throws an exception.)
*
* <p>The implementor must also ensure that the relation is transitive:
* {@code (x.compareTo(y) > 0 && y.compareTo(z) > 0)} implies
* {@code x.compareTo(z) > 0}.
*
* <p>Finally, the implementor must ensure that {@code
* x.compareTo(y)==0} implies that {@code signum(x.compareTo(z))
* == signum(y.compareTo(z))}, for all {@code z}.
*
* @apiNote
* It is strongly recommended, but <i>not</i> strictly required that
* {@code (x.compareTo(y)==0) == (x.equals(y))}. Generally speaking, any
* class that implements the {@code Comparable} interface and violates
* this condition should clearly indicate this fact. The recommended
* language is "Note: this class has a natural ordering that is
* inconsistent with equals."
*
* ......
*/
public int compareTo(T o);
}
跟前面几个方法类似,compareTo
方法也有一些约定,从上面的 Java Doc 中也能看到,对于比较的对象 x,y 和 z:
1)对称性:signum(x.compareTo(y)) == -signum(y.compareTo(x))
,这一条也暗示着当 x.compareTo(y) 抛出异常时,y.compareTo(x) 也必须抛出异常;
2)传递性:如果 x.compareTo(y) > 0
且 y.compareTo(z) > 0
,那么 x.compareTo(z) > 0
;
3)强烈建议当 x.compareTo(y) == 0
时,尽量保证 x.equals(y) == true
。
一般情况下,第三条也是能满足的,但也有例外情况,比如 BigDecimal
类:
class ComparableTests {
@Test
void should_storage_with_set() {
addAndDisplay(new HashSet<>());
addAndDisplay(new TreeSet<>());
}
private static void addAndDisplay(Set<BigDecimal> set) {
set.add(new BigDecimal("3.0"));
set.add(new BigDecimal("3.00"));
log.info("{}: {}", set.getClass().getSimpleName(), set);
}
}
运行结果如下:
12:11:03.755 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - HashSet: [3.0, 3.00]
12:11:03.756 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - TreeSet: [3.0]
可以看到,当使用 HashSet
存储 BigDecimal
时,3.0 和 3.00 是两个不同的元素,但使用 TreeSet
这种有序集合时,3.0 和 3.00 则是同一个元素。之所以出现这种情况,是因为 BigDecimal
违反了等同性约定:
BigDecimal bigDecimal = new BigDecimal("3.0");
BigDecimal bigDecimal2 = new BigDecimal("3.00");
log.info("bigDecimal.compareTo(bigDecimal2): {}", bigDecimal.compareTo(bigDecimal2));
log.info("bigDecimal.equals(bigDecimal2): {}", bigDecimal.equals(bigDecimal2));
// 运行结果
// 12:22:08.758 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - bigDecimal.compareTo(bigDecimal2): 0
// 12:22:08.758 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - bigDecimal.equals(bigDecimal2): false
而 HashSet 与 TreeSet 在存储元素时的差异在于,前者使用 equals
比较元素是否相等,后者则使用 compareTo
进行元素的位置比较。
当我们给一个类实现 Comparable
接口后,就可以跟许多有序相关的集合类或工具类进行协作,快速完成相关功能,比如排序:
public class FooBar implements Comparable<FooBar> {
private String name;
private int score;
private final List<String> values;
public FooBar(String name, int score) {
this.name = name;
this.score = score;
this.values = new ArrayList<>();
}
public String getNameAndScore() {
return name + "-" + score;
}
@Override
public int compareTo(FooBar o) {
// Warning: don't use the difference between two variables like below
// return this.score - o.score;
return Integer.compare(this.score, o.score);
}
}
class ComparableTests {
@Test
void should_sort() {
List<FooBar> fooBars = new ArrayList<>();
fooBars.add(new FooBar("Jack.马", 56));
fooBars.add(new FooBar("Andrew", 77));
fooBars.add(new FooBar("An子奇", 56));
fooBars.add(new FooBar("Tim", 98));
log.info("fooBars: {}", fooBars.stream().map(FooBar::getNameAndScore).collect(Collectors.joining(", ")));
Collections.sort(fooBars);
log.info("sorted fooBars: {}", fooBars.stream().map(FooBar::getNameAndScore).collect(Collectors.joining(", ")));
}
}
运行结果如下所示:
15:55:36.723 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - fooBars: Jack.马-56, Andrew-77, An子奇-56, Tim-98
15:55:36.725 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - sorted fooBars: Jack.马-56, An子奇-56, Andrew-77, Tim-98
需要注意的是,如果存在数值计算,不要使用差值,这很有可能会导致溢出。由于基础类型的包装类型都提供了静态的 Compare
方法,所以直接使用这些静态方法则是更好的选择。如果不实现 Comparable
接口,直接使用 Arrays#sort
或 Collections#sort
等方法会有什么后果?前者会提示 java.lang.ClassCastException:xxx cannot be cast to class java.lang.Comparable
,而后者则直接无法编译。
如果一个对象中有多个域需要比较,最好的方式是按照域的重要性逐个比较,比如:
@Override
public int compareTo(FooBar o) {
// Warning: don't use the difference between two variables like below
// return this.score - o.score;
int result = Integer.compare(this.score, o.score);
if (result == 0) {
result = this.name.compareTo(o.name);
}
return result;
}
此时的运行结果如下,可以看到,An子奇
排到了 Jack.马
之前(虽然他们的 score 值相同):
15:57:09.275 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - fooBars: Jack.马-56, Andrew-77, An子奇-56, Tim-98
15:57:09.277 [Test worker] INFO jvm.tech.demonlee.tmp.ComparableTests - sorted fooBars: An子奇-56, Jack.马-56, Andrew-77, Tim-98
由于 Java 8 升级了函数式编程,且提供了 Comparator
函数式接口,所以我们可以借助语法糖重新实现上面的 compareTo
方法:
@Override
public int compareTo(FooBar o) {
return Comparator.comparingInt((FooBar o1) -> o1.score).thenComparing((FooBar o1) -> o1.name).compare(this, o);
}
当然,为了提高运行性能,我们也可以直接缓存一个 Comparator
对象实例作为属性域,然后在 compareTo
方法中直接使用。
好,Object
基类中的通用方法就简单总结到这里, 下期再见。