前言
自己学习effective java,就分享一下自己学的内容。
equals方法
比较两个对象是否相等,如果不重写equals()方法就使用默认方法
public boolean equals(Object obj) {
return (this == obj);
}
一般我们什么时候回用到这个equals方法呢?
最简单的例子就是集合对象里,这里面用到的最多:
// 给出一个test方法
Student s1 = new Student("a",1);
Student s2 = new Student("a",1);
List<Student> studentList = Lists.newArrayList();
studentList.add(s1);
studentList.add(s2);
studentList.contains(s1);
studentList.indexOf(s1);
studentList.remove(s1);
其中indexOf方法源码如下:
/**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i])) // 注意这里调用了 equals 方法
return i;
}
return -1;
}
equals方法不改写默认构造方法,会有什么问题产生:
Student s1 = new Student("a",1);
Student s2 = new Student("a",1);
System.out.println("s1.equals(s1)="+s1.equals(s1)); // true
System.out.println("s1.equals(s2)="+s1.equals(s2)); // false
从上述代码里我们可以看到默认构造函数会比较对象本身的内存地址,有时候我们可能并不需要这样的比较,特别是当我们编写一个新的类的时候,需要一个业务场景比较对象的内容相等就可以,这样我们就可以改写equals方法,如果你要自己覆盖equals方法,就要遵守以下原则:
(1) 自反性: x.equals(x) == true
(2) 对称性: x.equals(y) == y.equals(x) ,返回值相等
(3) 传递性: x.equals(y) == true,y.equals(z) == true,那么x.equals(z) == true
(4) 一致性: 对象x和y在equals()中使用的信息无改变,x.equals(y)的值始终不变
(5) 非null : x不是null,y是null,那么x.equals(y)必须为false
如果没有遵守上述的原则,会有一些问题:
(1) 违反自反性,list contains 方法 就会认为这个对象不存在,因为集合中存进去的obj对象,不能判断 obj.equals(obj) 。
(2)对称性:
父类:
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof People) {
People anotherPerson = (People) obj;
if (this.getName().equals(anotherPerson.getName()) ) {
System.out.println("p相等");
return true;
}
}
return false;
}
子类:
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Student) {
Student studentTwo = (Student) obj;
if (this.getName().equals(studentTwo.getName())
&& this.getId() == studentTwo.getId()) {
return true;
}
}
return false;
}
测试方法:
Student s1 = new Student("a",1);
Student s2 = new Student("a",1);
System.out.println("s1.equals(s1)="+s1.equals(s1));
System.out.println("s1.equals(s2)="+s1.equals(s2));
People p1 = new People();
p1.setName("a");
System.out.println("p1.equals(s1)="+p1.equals(s1));
System.out.println("s1.equals(p1)="+s1.equals(p1));
传递性:
父类:
public class Point {
private final int x;
private final int y;
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return (p.x == x) && (p.y == y);
}
}
子类:
public class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
return super.equals(o) && (((ColorPoint) o).color == color);
}
}
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println("p1.equals(p2)="+ p1.equals(p2)); // true
System.out.println("p2.equals(p3)="+ p2.equals(p3)); // true
System.out.println("p1.equals(p3)="+ p1.equals(p3)); // false
上述代码违反了传递性,就是因为子类 增加了新的属性,这些属性不会出现在子类与父类equals方法,但子类与子类的equals方法就会用的上这些属性。
另一种方法就是讲对上述的类中的equals方法中的instanceof 替换为 getClass,这样每次只会比较具体的实现类,
父类:
public class Point {
private final int x;
private final int y;
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) { // 注意
return false;
}
Point p = (Point) o;
return (p.x == x) && (p.y == y);
}
}
子类:
public class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) { // 注意
return false;
}
return super.equals(o) && (((ColorPoint) o).color == color);
}
}
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println("p1.equals(p2)="+ p1.equals(p2)); // true
System.out.println("p2.equals(p3)="+ p2.equals(p3)); // true
System.out.println("p1.equals(p3)="+ p1.equals(p3)); // true
这样就可以再子类中增加 新的属性值,又同时可以保证传递性,但是这样会有一个另一个bug!看例子:
工具类:
private static final Set<Point> unitCircle = Set.of( // 集合中包括了所有的点
new Point( 1, 0), new Point( 0, 1),
new Point(-1, 0), new Point( 0, -1));
public static Boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
子类:
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public static int numberCreated() {
return counter.get();
}
}
实现类:
CounterPoint cp1 =new CounterPoint(1,0);
onUnitCircle.(cp1); // 子类实现类判断是否包含
如果我们把子类实现类放进去就会判断为false,用的父类的equals方法。
所以还有一种方案:复合优先继承。
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color); // 属性分别调用自己的equals
}
}
采用复合形式的类中各个属性分别调用自己的equalse方法,还可以随意扩展属性值,还可以在无任何属性值的一个抽象类的子类中随意添加属性值,这样也不会违法equals方法。这个确认一下。
那么我们有什么诀窍来写equals方法:
- 使用“==”操作符检查入参是否是对象的引用
- 使用instanceof 操作符检查入参是否是正确的类型
- 把参数转换为正确的类型来比较
- 类中所有的关键域都要检查,都匹配上了返回true 接下里我们可以看下ArrayList中的equals方法:
public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator<?> e2 = ((List<?>) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); }
hashcode方法
给每一个对象生成一个code,默认方法的实现是在C/C++代码里,hashCode()返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现,默认的代码如下:
public native int hashCode();
hashCode的重写约定:
(1) java应用运行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须返回相同的整数。
(2)如果两个对象调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
(3)尽可能为不相等的对象产生不相等的散列码,这样可以提高散列表性能。
hashCode()另一方面更好的支持哈希表(如HashMap、HashSet等),利用key的hashcode来散列,可以很明确相同对象的code一定是相等的,所以Java 建议重写了equals()方法,也要重写hashCode()方法
如果重写了equals(),没有重写hashCode() ?
Boy b1 = new Boy("a",1); Boy b2 = new Boy("a",1); Set<Boy> boySet = Sets.newHashSet(); boySet.add(b1); boySet.add(b2); System.out.println("b1.equals(b2)="+b1.equals(b2)); // b1.equals(b2)=true System.out.println("boySet.size() = "+boySet.size());// boySet.size() = 2 System.out.println("b1.code="+b1.hashCode()); // b1.code=1673605040 System.out.println("b2.code="+b2.hashCode()); // b2.code=186276003
这里改写了equals方法,两者是相等的,但是没有改写hashcode方法,放到hashSet对象里,他就认为是两个对象。
重写对象 hashCode()一些技巧:如果对象中某个属性值:
(1) 基本类型:调用装箱类型的hashCode()。
(2) 对象引用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0。
(3) 数组,那么需要为每个元素当做单独的域来处理。java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算。我们看下String的hashCode的方法:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
toString方法
把对象的所有属性值输出来,这个基本上现在都是用阿里巴巴的一个工具包fastjson来实现,不需要自己的类中额外实现toString方法,不过这个工具打出的信息太多了,你也可以自己实现toString方法把关键信息打印出来。
clone方法
Cloneable接口只有一个方法:clone(),如下图所示:
public interface Cloneable { }
这个方法是表明调用这个方法可以得到自己的逐域拷贝–新的对象,如果要改写这个方法,可能要遵守一些协议约定,这些协议约定 不可实施、复杂、无文档特殊说明,可能会让你觉得clone()方法可以 不需要调用构造器就可以创建对象,我们看下约定:
- (x.clone() != x ) == true
- (x.clone().getClass() == x.getClass() ) == true
- (x.clone().equals(x)) == true
但这些预定是不一定要遵守的,由此我们可以得到两种拷贝:
- 浅拷贝:浅拷贝只是单纯的对于对象的拷贝,对象属性对于其它对象的引用并没有进行拷贝(也就是说浅拷贝创建的对象和原来对象指向不同的地址空间,但是对象属性里面对其它对象的引用【引用属性】指向的还是同一个地址空间);
- 深拷贝:深拷贝是指拷贝创造出来的对象和原来对象是完全不同的,不仅拷贝创建的对象和原来对象指向的地址不同,引用属性指向的地址空间也是不一样的。如果引用属性里面还含有引用属性,那么该引用属性指向的地址空间也是不同,一次类推……。可以理解成深拷贝是浅拷贝的递归。
很明显浅拷贝容易造成引用对象被修改,而影响了拷贝的新对象中的对象属性,深拷贝就比较好,但用深拷贝还必须注意一个问题就是链表数组的问题,在hashtab中每个桶有一个链表,数组中存储的是链表的第一个项,如果拷贝的时候只拷贝了链表的第一个项,链表中的其他对象不考虑,就会出现浅拷贝的问题,需要对链表中的对象进行拷贝。
另一种方式就是利用拷贝构造器、拷贝工厂来拷贝对象,不需要继承Cloneable接口,不必遵守相关约定,减少依赖。
hashTable类中的clone方法:public synchronized Object clone() { try { Hashtable<?,?> t = (Hashtable<?,?>)super.clone(); t.table = new Entry<?,?>[table.length]; for (int i = table.length ; i-- > 0 ; ) { t.table[i] = (table[i] != null) ? (Entry<?,?>) table[i].clone() : null; } t.keySet = null; t.entrySet = null; t.values = null; t.modCount = 0; return t; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
Comparable接口
Comparable接口中唯一的compareTo方法,用来排序。默认的接口:
public interface Comparable<T> { public int compareTo(T o); }
书写compareTo方法有一下约定:
- (1) 自反性 : a.compareTo(a) == 0
- (2) 对称性 : a > b 反过来 b > a ,a = b 反过来 b = a
- (3) 传递性 : a > b , b > z - - - > a > z
- (4)强烈建议 :( x.compareTo(y) == 0 ) == ( x.equals(y) )
如果违反了上述约定,就会有一些问题:
BigDecimal a1 = BigDecimal.valueOf(1.0); BigDecimal a2 = BigDecimal.valueOf(1.00); TreeSet<BigDecimal> tSet = new TreeSet(); tSet.add(a1); tSet.add(a2); System.out.println("tSet.size = "+tSet.size()); // tSet.size = 1 // 其中BigDecimal的compareTo方法如下: public int compareTo(BigDecimal val) { // Quick path for equal scale and non-inflated case. if (scale == val.scale) { long xs = intCompact; long ys = val.intCompact; if (xs != INFLATED && ys != INFLATED) return xs != ys ? ((xs > ys) ? 1 : -1) : 0; } int xsign = this.signum(); int ysign = val.signum(); if (xsign != ysign) return (xsign > ysign) ? 1 : -1; if (xsign == 0) return 0; int cmp = compareMagnitude(val); return (xsign > 0) ? cmp : -cmp; }
hashCode不一样,会认为这两个相同的元素是不同的对象,但其实我们改写了equals方法,这两个对象是相等的,没有覆盖hashcode极有可能会影响你的业务逻辑。
除了实现comparable接口还有Comparator接口用来作为比较器。
Collections.sort(Lists.newArrayList(testSet), new Comparator<Police>() { @Override public int compare(Police obj1, Police obj2) { return obj1.getID().compareTo(obj2.getID()); } }); Collections.sort(Lists.newArrayList(testSet), (obj1, obj2) -> obj1.getID().compareTo(obj2.getID()));
我们看下TreeMap的get操作:
public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); } final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); // 注意这比较 if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; }
TreeMap会用类型的compareTo方法来比较对象,从而顺着树结构往下查找元素。
参考博客
主要是effective java 第二版 第三版,欢迎大家多看看,这书现在还有用,讲解一些基本问题还是不错的,就是比较拗口。