注意:重写equals、hashCode、clone、toString等方法都可以直接在IDE右键-->Generate...中自动生成
第10条:覆盖equals时请遵守通用约定
1)类的每个实例本质上是唯一的
2)类没有必要提供“逻辑相等”的测试功能
3)超类已经覆盖了equals,超类的行为对于这个类也是合适的
4)类是私有的,或者是包级私有的,可以确定它的equals方法永远不会被调用
equals方法的通用约定:自反性、对称性、传递性、一致性、非空性(对于任何非null的引用值x,x.equals(null)必须返回false),覆盖equals方法的步骤:
1、使用==操作符检查“参数是否为这个对象的引用”。如果是,返回true,这是性能优化,如果比较操作很昂贵就值得这么做
2、使用instanceof操作符检查“参数是否为正确的类型”。如果不是,返回false。一般所谓“正确的类型”是指equals方法所在的那个类
3、把参数转换成正确的类型。因为转换之前进行过instance测试,所以确保会成功
4、对于该类中的每个“关键”(significant)域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true;否则返回false。如果第2步中的类型是个接口,就必须通过接口方法访问参数中的域;如果该类型是个类,也许就能够直接访问参数中的域,这取决于它们的可访问性
public class PhoneNumber {
private final short areaCode,prefix,lineNum;
public PhoneNumber(int areaCode,int prefix,int lineNum){
this.areaCode=rangeCheck(areaCode,999,"area code");
this.prefix=rangeCheck(prefix,999,"prefix");
this.lineNum=rangeCheck(lineNum,999,"line num");
}
private static short rangeCheck(int val,int max,String arg) {
if (val<0||val>max){
throw new IllegalArgumentException(arg+":"+val);
}
return (short) val;
}
@Override
public boolean equals(Object o){
if (o==this){
return true;
}
if (!(o instanceof PhoneNumber)){
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum==lineNum && pn.prefix==prefix&&pn.areaCode==areaCode;
}
}
注意:
1、覆盖equals方法的时候,也要覆盖hashcode方法
2、不要企图让equals方法过于智能。一般简单的测试域中的值是否相等就好
3、不要将equals声明中的object对象替换为其他的类型
/**
*这里的public boolean equals(Object o)换成了MyClass,无法正常工作
*原因:这个方法没有覆盖(override)Object.equals,它的参数应该是Object类型
*相反,它重载(overload)了Object.equals
*/
@Override
public boolean equals(MyClass o){
if (o==this){
return true;
}
if (!(o instanceof PhoneNumber)){
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum==lineNum && pn.prefix==prefix&&pn.areaCode==areaCode;
}
第11条:覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。如果不这样做,就会违反hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括HashMap和HashSet。
- 因没有覆盖hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散列码
- 不要试图从散列码计算中排除掉一个对象的关键域来提高性能
- 不要对hashCode方法的返回值做出具体的规定,因为客户端无法理所当然的依赖它,这样可以为修改提供灵活性
覆盖hashCode方法的步骤:
- 声明一个int变量并命名为result,将它初始化为对象中第一个关键域的散列码c,如步骤2.A所示(第10条:关键域是指影响equals比较的域)
- 对象中剩下的每一个关键域f都完成以下步骤
- 为该域计算int类型的散列码c:
- 如果该域是基本类型,则计算Type.hashCode(f),这里的Type是装箱基本类型的类,与f的类型相对应
- 如果该域是一个对象引用,并且该类的equals方法通过递归的调用equals的方式来比较这个域,则同样为这个域递归的调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,通常是0)
- 如果该域是一个数组,则要把每一个元素当作单独的域里处理。即递归的调用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.B中的做法把这些散列值组合起来。如果数组域中没有重要的元素,可以使用一个常量,但最好不要用0.如果数组域中的所有元素都很重要,可以使用Arrays.hashCode方法
- 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中
使用31是因为它是一个寄素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于位运算。使用素数的好处并不很明显,但是习惯上都使用素数来计算散列结果。31可以用移位和减法来代替乘法,得到更好的性能:31 * i == (i << 5) -i,现代的虚拟机可以自动完成这种优化。
result = 31 * result + c;
3)返回result
public class PhoneNumber {
private final short areaCode,prefix,lineNum;
public PhoneNumber(int areaCode,int prefix,int lineNum){
this.areaCode=rangeCheck(areaCode,999,"area code");
this.prefix=rangeCheck(prefix,999,"prefix");
this.lineNum=rangeCheck(lineNum,999,"line num");
}
private static short rangeCheck(int val,int max,String arg) {
if (val<0||val>max){
throw new IllegalArgumentException(arg+":"+val);
}
return (short) val;
}
@Override
public boolean equals(Object o){
if (o==this){
return true;
}
if (!(o instanceof PhoneNumber)){
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum==lineNum && pn.prefix==prefix&&pn.areaCode==areaCode;
}
/**
* 常用hashCode方法
* @return
*/
@Override
public int hashCode(){
// 初始化为对象中第一个关键域的散列码
int result = Short.hashCode(areaCode);
result = 31*result+Short.hashCode(prefix);
result = 31*result+Short.hashCode(lineNum);
return result;
}
/**
* Objects.hash():可以传递任意个参数,并为它们返回一个散列码
* 速度略慢,因为它们会引发数组的创建,以便传入数目可变的参数,如果参数中有基本类型,还需要装箱和拆箱。
* 不注重性能的时候可以用这个方法
* @return
*/
@Override
public int hashCode(){
return Objects.hash(lineNum,prefix,areaCode);
}
}
第12条:始终要覆盖toString
重写了toString方法,最好注释toString输出的格式,调用toString也不用System.out.println(entity.toString);直接System.out.println(entity);因为它会自动调用toString方法。
- 提供好的toString实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试
- 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息
- 无论是否指明格式,都应该在文档注释中表明重写toString的意图,并为toString返回值中包含的所有信息提供一种可以通过编程访问之的途径
@Override
public String toString() {
return "PhoneNumber{" +
"areaCode=" + areaCode +
", prefix=" + prefix +
", lineNum=" + lineNum +
'}';
}
第13条:谨慎的覆盖clone
实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法,不可变的类永远都不应该提供clone方法。步骤:
1、类实现Cloneable接口
2、如果类中属性只有基本类型(如int/String/long等),直接在方法中return (entity)super.clone(),注意处理异常CloneNotSupportedException
/**
* 自动生成的clone方法是这样的,我们可以把它修改
* 1)protected --> public
* 2)返回值Object --> 对应的entity
* 3)异常不要抛出throws --> catch
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public PhoneNumber clone(){
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
3、如果对象中包含的域引用了可变的对象(有属性是数组),就需要在数组中递归的调用clone方法进行拷贝
因为clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确的创建被克咯对象中的约束条件
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT = 16;
@Override
public Stack clone(){
Stack result = null;
try {
result = (Stack) super.clone();
// 递归的拷贝每一个值
result.elements = elements.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return result;
}
}
4、如果对象中包含有链表结构,那就需要进行深度拷贝,代码clone要具体到链表中每一个对象
// 必须实现Cloneable接口
public class HashTable implements Cloneable{
private Entry[] buckets;
private static class Entry{
final Object key;
Object value;
Entry next;
Entry(Object key,Object value,Entry next){
this.key=key;
this.value=value;
this.next=next;
}
/**
* 在定义的链表属性中添加深度拷贝方法,用迭代代替递归
*/
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方法
*/
@Override
public HashTable clone() throws CloneNotSupportedException {
// 基本属性直接super.clone
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;
}
}
建议使用:对象拷贝的更好的办法是提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,如
/**
* 拷贝构造器
* @param hashTable
*/
public HashTable(HashTable hashTable){
this.buckets = hashTable.buckets;
}
拷贝工厂
public static HashTable newInstance(HashTable hashTable){
// 加了static关键字就成了拷贝工厂了
}
第14条:考虑实现Comparable接口
对一个List<Entity>中的实体类某个或者某几个字段进行分组,然后再对实体类的另外的属性进行排序,并重新赋值
public class Test{
public void test(List<Entity> entities) {
/**
* 确保要比较的字段不为null之后可以直接使用lambda
*/
/*entities = Comparator.comparingInt(entity->fetchGroupKey(entity))
.thenComparingInt(entity->entity.getTwoThing());*/
//-----不使用lambda进行分组排序的方法-------------------
/**
* OneThing、TwoThing字段都是String类型,ThreeDate是时间类型
*
* 1、对List<Entity>里的OneThing、ThreeDate属性相等的字段(唯一key)进行分组
* 2、然后根据twoThing属性排序(从小到大)
*/
entities.sort(this::compare);
/**
* 3、对根据OneThing、ThreeDate分好组的实体类的TwoThing重新赋值
*/
int count = 1;
String preKey = null;
for (int i = 0; i < entities.size(); i++) {
Entity entity = entities.get(i);
if (Objects.equals(preKey, fetchGroupKey(entity))) {
// count对相同key、已排序的组计数
count++;
} else {
count = 1;
}
// 已经分组排序了,所以可以使用preKey作为比较计数的判断条件,一个preKey遍历一组
preKey = fetchGroupKey(entity);
/**
* 对TwoThing字段赋值规则:数量count不足两位(每组的实体类数量少于10),前面补0,并将其转为String类型
* 如果多于10,就正常赋值
* 值得形式如:01 02 03...10 11 12...
*/
entity.setTwoThing(String.format("%02d", count));
}
}
private int compare(Entity o1, Entity o2) {
// 以o1.getOneThing()为比较条件,进行分组
int is = fetchGroupKey(o1).compareTo(fetchGroupKey(o2));
// 这两个实体类不是同一组的,直接返回,继续比较下面两组
if (is != 0) {
return is;
} else {
// 这两个实体类是同一组的,进行排序
// 以TwoThing字段的大小作为分组后的排序
String o1Number = o1.getTwoThing();
String o2Number = o2.getTwoThing();
// 对字段进行判空,防止为null时候比较产生异常
if (null == o1Number && null != o2Number) {
return -1;
}
if (null != o1Number && null == o2Number) {
return 1;
}
if (null == o1Number && null == o2Number) {
return 0;
}
return Integer.parseInt(o1.getTwoThing()) - Integer.parseInt(o2.getTwoThing());
}
}
/**
* 对该实体类的某几个字段组装唯一key,作为分组条件
*/
private String fetchGroupKey(Entity entity) {
// 这里用getTime()是因为效率稍微比toString()高一些
return entity.getOneThing() + "-" + (null == entity.getDate() ? "" : entity.getThreeDate().getTime());
}
}
使用Lambda表达式进行分组排序,记得使用Java的静态导入(static import)设施,通过静态比较强构造方法的简单的名称就可以对它们进行引用。简单点说就是使用static final关键字修饰
/**
* 确保要比较的字段不为null之后可以直接使用lambda
*/
private static final List<Entity> entities =
Comparator.comparingInt(entity->fetchGroupKey(entity))
.thenComparingInt(entity->entity.getTwoThing());