11、重写equals
时重写hashCode
方法
在每一个重写了equals方法的类中,应当重写这个类的hashCode
方法。如果不这么做,那么将会违背Object.hashCode
的通用约定,从而导致某一些基于hash散列的集合无法正常运行,比如说HashSet
,HashMap
,HashTable
。
规范:
- 在程序执行的这段时间,如果对象
equals
方法进行的比较信息没有被改变的话,那么这个对象的hashCode
该返回同一个整数。 - 如果这个应用程序多次执行,那么没有必要保证每一次的
hashCode
都是一样的。 - 如果两个对象通过equals方法比较是相等的,那么他们的
hashCode
应该一样 - 如果两个对象通过equals方法比较是不相等的,那么他们的
hashCode
不应该一样。
对于基于hash散列的类来说,如果不重写hashCode
方法的话,那么即便他们通过equals
方法得到的结果是true,那么将他们作为hashMap
的key也无法拿到正确的值,因为他们只是逻辑上相等,而hashMap
是基于hashCode
寻址的。
hashCode
的计算:
-
为对象中的每一个关键域f,计算其散列码c
result = 31 * result + c;
-
返回 result
注意:
- 如果一个域的值是由其他域演算出来的话,那么这个域不需要用于计算
hashCode
- 如果一个域并没有在equals方法中使用,那么这个域不应该用于计算
hashCode
- 不要排除一些关键的属性来提高
hashCode
的性能,这可能会降低这个方法的可靠性 - 在编写完
hashCode
之后,应当测试hashCode
功能的正确性 - 如果一个对象计算
hashCode
的代价比较大的话,应当缓存这个hashCode
12、始终重写toString
方法
虽然Object
类提供了toString
方法的实现,但是在某些情况它返回的toString
并没有什么乱用。提供一个良好的toString
方法可以使类更易于使用和调试。
-
toString
方法应当返回对象中包含的所有需要关注的信息 - 应当在文档中指定
toString
方法返回的格式 - 在静态工具类中,编写
toString
方法是没有任何意义的。 - 应当在任何抽象类型中定义
toString方法
,使得子类共享一个公共字符串表示形式 - 除非父类已经实现了
toString
方法,否则应该在每一个实例化的子类中重写toString
方法。
13、谨慎地重写clone
首先需要指出,这个条目是基于Cloneable
接口或者说clone
方法而列出的。Cloneable
接口是一个空的接口,它仅仅用来表明这个对象是允许被克隆的。真正的clone
方法的提供是在Object类中。这一点也被作者在书中描述为设计上面的缺陷。
Cloneable
接口:
这是一个并未包含任何方法的接口,它的唯一作用就是决定Object
类中clone
方法实现的行为。换句话说,如果一个类实现了Cloneable
接口,那么它就应该在clone方法里面返回对该对象的逐域拷贝,否则就会抛出CloneNotSupportedException
异常。
- 这是一种比较极端的写法,因为它违背了定义接口的作用。
- 这是一种可以不调用构造器就创建对象的方法。
对于clone
方法的一些约束:
(x.clone() != x) == true
(x.clone().getClass() == x.getClass()) == true
x.clone().equals(x) == true
解读:
不得不说,这本书的翻译是真的烂。_ (:з)∠) _
- 上面提到的不调用构造方法就可以创建对象的规定其实太过于强硬,或者享有了太大的特权,想象你好不容易实现了一个单例,并通私有构造方法并抛出异常,甚至提供了
readResolve
这个方法来保证反序列化也是单例。但是一旦你实现了Cloneable
,接口并重写了clone方法,那么你好不容易设计的单例会因为clone而失效。作者指出行为良好的clone方法,应该是可以调用构造器来创建对象的,但是遗憾,clone并没有这么做。 - 作者强调(通常情况下)
(x.clone().getClass() == x.getClass()) == true
这个规定太过于软弱。这里我理解了半天。为什么叫过于软弱,因为它不是强制要求的,因为clone指出了不通过构造器就创建对象,但是却允许了(x.clone().getClass() == x.getClass()) != true
。举个例子:比如我有一个child类,他继承了他的父类,但是并没有重写clone
方法,那么这个方法将最终调用父类的clone
。但是很遗憾,父类的设计者并未考虑到这个问题,因为他约定clone
的一些约束,允许上面的不等条件出现,所以他使用了构造方法来clone了对象的副本。那么当子类调用clone
方法的时候,你会惊讶的发现居然返回的是父类的对象,甚至你无法通过强制类型转化将其转化过来。 - 为了避免上面的情况,保证子类通过调用
super.clone()
也返回自己的实例。我们的父类也应该调用自己的super.clone()
(父类的父类的clone方法)。在这种层层传递下,所有的super.clone()
最终都会调用Object.clone()
方法,这样就能保证在整个类的层级结构中,所有的子类的clone()
方法最终均会返回Object的对象,由于Objcet.clone()
返回的是对象的逐域拷贝(也就是对整块内存的复制),所以最终我们只需要进行强制类型转化即可。 - 很遗憾的是,
Cloneable
接口并没有清楚的指出一个类实现这个接口应该承担什么责任。
浅克隆:
这里作者列举了一个Stack
类的例子:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
我们可以看到,Stack类持有Object数组的应用。当我们克隆这个对象的时候,理想情况下,Stack的elements也会跟随一起被克隆,然而很遗憾,克隆出来的的对象和原来的对象持有相同element对象的引用,也就是说,克隆的时候,只传递了引用。
深克隆:
其实就是针对上面问题的一个解决方案罢了。
对于对象持有的对象引用,我们应当递归(或迭代)地调用这些对象的clone
方法。
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
克隆的替代方案:
- 对于那些确定是不可变的类,最好不要实现clone方法,因为这没有意义。
- 使用拷贝构造器或者拷贝工厂
- 使用基于接口的拷贝构造器(转换构造器)或者拷贝工厂(转换工厂)。
14、考虑实现Comparable接口
compareTo
方法并没有在Object
类中声明,它是Comparable
接口中的唯一方法,其实这个是很好理解的,因为我们并不需要每一个对象都是可以比较大小的。
compareT
方法
int compareTo(T t)
可以看到,这个方法是泛型的,并且返回的结果是int类型。其值和比较的结果关系如下:
- 负数:<
- 0:=
- 正数:>
好处:
通过实现Comparable
接口,可以让类与所有依赖此接口的通用算法和集合实现来进行相互操作。并且几乎所有的JAVA平台类库中的所有值类都实现了这个接口。
要求:
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) == true
(x.compareTo(y) > 0 && y.compareTo(z) > 0) == (x.compareTo(z)>0) == true
x.compareTo(y) == 0 —> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
-
(x.compareTo(y) == 0) == (x.equals(y))
注意,与equals
方法不同的是,equals
方法有可能花月不同类型的对象,而compareTo
遇到不同类型的对象的时候,会直接抛出异常。
注意:
-
不要使用两个值之间的差值来返回
compareTo
的结果,这种写法可能会导致整数的最大长度溢出。 - 使用
compareTo
方法的时候,应当避免使用 “<”(小于)或">"(大于)。在具体的值进行比较的时候,应当使用静态的compare
或者Comparator
接口中的构建方法。
15、使类和成员的可访问性最小化
一个良好设计的类应该隐藏它的所有实现细节,仅仅对外暴露API,这样会比较干净。其他的组件,通过API和他们进行交互,并且对他们的内部工作应当一无所知。这个概念被称为封装。
封装的优势:
- 将组成系统的组件分开,允许他们被独立的开发,测试,优化。
- 拥有不同功能的组件可以并行开发。
可访问性最小化原则:让每一个类或成员尽可能的不可访问,即尽可能的降低访问级别。
- 一个顶级类(.java文件中的直接定义的类)或接口的访问级别只能是
default
或者public
的。如果没有必要将其设置为public
,那么我们就应该将其设置为default
的。 - 当我们把一个类设置为
public
的时候,那么它就拥有导出API的功能,在这种情况下,我们应该有义务维护这个类,让其保持着API的兼容性。当然如果是default
的话就没有这个必要,因为它是包级私有的,并不拥有到处API的功能,仅仅是组件实现的一部分。 - 如果一个
default
修饰的顶级类只被一个类使用的话,那么我们应该考虑将这个类作为其使用者的私有静态嵌套类。 - 对于一个
public
修饰的类,我们在设计其成员的时候,应该尽可能的将其成员设置为私有的。
注意:
- 如果一个类存在一个是
public
且非final
的实例属性,那么我们就放弃了限制这个实例的属性被修改的能力,并且当这个实例的属性被修改的时候,我们也并没有解决的方案。 - 即便是类中的实例属性是
final
的,也只能保证它引用的对象无法改变而无法保证它引用的对象的属性保持不变。 - 对所有的静态属性也是如此。我们通过
public static final
来表示一个常量。但是,如果其引用的是一个对象,虽然对象的引用无法被修改,但是我们我发保证对象的属性保持不变。 - 所有的
public static final
修饰的长度不为0的数组,我们也应当注意,因为它是可变的。
网友评论