1.[强制] 关于hashCode和equals的处理,遵循如下规则:
- 只要重写equals,就必须重写hashCode。
- 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
- 如果自定义对象作为Map的键,那么必须重写hashCode和equals。
说明: String重写了hashCode和equals方法,所以我们可以非常愉快地使用String对象作为key来使用。
老四附言:
我们先来简单的复习一下Object的equals和hashCode方法。
equals和hashCode的源码如下:
1 2 3 4 |
public boolean equals(Object obj) { return (this == obj); } public native int hashCode(); |
从源码中我们可以看到equals方法是比较对象的引用是否一样,说白了就是Object中的equals方法跟我们经常用的==是一样的,都是要比较对象的引用。这里先排除String、Integer等基本数据类型的重写(它们是重写之后进行值比较)。Object默认提供的equals()只是比较对象的地址,因此在实际应用中常常需要重写equals()方法,重写equals的时候,相等的条件是由你的业务要求决定的,因此equals()的实现也是由业务要求决定的。通常而言,正确地重写equals()方法应该满足几个数学相等相关的特性条件:
- 自反性: obj.equals(obj)结果一定是true。
- 对称性: 如果obj1.equals(obj2)结果为true,那么obj2.equals(obj1)也肯定是true啊。
- 传递性: 如果obj1.equals(obj2)结果为true,obj2.equals(obj3)结果也为true,那obj1.equals(obj3)肯定也是true啊。
- 一致性: 对任意的obj1和obj2,如果对象中用于等价比较的信息没有改变,那么无论调用obj1.equals(obj2)多少多少次,返回的结果是要保持一致的。即要么一直是true,要么一直是false。
- 以上几条都不包含obj为null的情况,而且对于任何非null的obj,obj.equals(null)都应该返回false。
那么为什么要求重写equals的时候,就必须重写hashCode方法呢?
首先我们都知道Java中的集合有两类,一类是List,一类是Set,Set的设计是不能重复的,那么我们要想保证元素不重复应该根据什么来判断呢?这个时候一个叫哈希(Hash)的人提出了一个散列算法,为了纪念他,也叫哈希算法,简单点说这个散列算法就是将特定的数据通过散列计算将数据存储到对应的地址上去,并返回数据存储的地址值。试想一下,如果一个Set中已经加入了一千甚至更多的元素,此时再添加一个元素,如果我们只用equals来判断的话那么我们前一千个元素需要每个都比对一遍,显然这不是我们想要的,而hashCode方法,通过计算,如果当前位置没有数据,就放入到该位置返回地址。如果该位置已经有人了,那么然后再调用equals方法来判断是否相等,如果相同可以不存,不相同就换个地方存储就好了。
那么为什么还要要求Set里面存放的对象也必须重写equals和hashCode方法呢?我们可以稍微深入性的来分析一下:
从源码中我们能见到hashCode是本地方法,跟我们运行的本地及其相关,大量的实践也表明Object类定义的hashCode方法对于不同的对象会返回不同的哈希值。在集合查找过程中使用hashCode可以大幅度降低equals判断,提高查找效率。所以在Java中就有了这样的一个规定: 如果两个对象通过equals()方法返回true,这两个对象的hashCode值也应该相同。所以如果两个对象的equals()方法返回true,但是hashCode返回的值不相等就会导致Set把两个对象保存在两个不同的位置,这与Set的设计规则不符合。同理,反过来,如果两个对象的哈希值相同,但是equals返回false,这样就会导致Set将会用链表的形式在同一位置存放两个对象,这样虽然不算错,但是效率大打折扣。还有就是之前说到,Object的hashCode()方法是本地方法,如果Set中的对象不重写equals()和hashCode()方法,即使我们业务上认为相等的对象也可能是不相等的。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package com.glorze.collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * Set中的对象需要重写equals()和hashCode()方法 * @ClassName TestHashSet * @author: glorze.com * @since: 2018年9月13日 下午9:40:08 */ public class TestHashSet { public static void main(String[] args) { Set<Student> studentSet = new HashSet<Student>(); studentSet.add(new Student(18, "四嫂")); studentSet.add(new Student(28, "高老四")); studentSet.add(new Student(38, "四叔")); studentSet.add(new Student(18, "四嫂")); Iterator<Student> it = studentSet.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } } class Student { private Integer age; private String name; public Student(Integer age, String name) { super(); this.age = age; this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } } |
以上代码运行结果:
com.glorze.collection.Student@7852e922
com.glorze.collection.Student@15db9742
com.glorze.collection.Student@6d06d69c
com.glorze.collection.Student@4e25154f
我们看到其实我们业务上应该认为最后一条四嫂的记录已经被添加,但是由于没有重写equals()和hashCode(),导致他们是不同的对象,都被添加进Set集合中了(本质是hashCode不同,因为new了两个对象,地址不一样的)。所以如果需要吧某个类的对象保存到Set集合中,不仅一定要重写这个类的equals()方法和hashCode()方法,而且应该尽量保证两个对象通过equals()返回true时,他们的hashCode()方法返回值也要相等。
说完以上这些,那么孤尽老师说的第三条也就不奇怪了。为什么?因为HashSet就是靠HashMap实现不允许存储重复元素的,我们都知道HashMap的键是不允许重复的,所以Map的中的对象肯定也必须要求重写equals()和hashCode()方法。
2.[强制] ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明: subList返回的是ArrayList的内部类SubList,并不是ArrayList,而是ArrayList的一个视图,对于SubList子列表的所有操作最终会反映到原列表上。
老四附言:
这里我们看一下ArrayList的即可了解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); } private class SubList extends AbstractList<E> implements RandomAccess { private final AbstractList<E> parent; private final int parentOffset; private final int offset; int size; SubList(AbstractList<E> parent,cint offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; } public E set(int index, E e) { rangeCheck(index); checkForComodification(); E oldValue = ArrayList.this.elementData(offset + index); ArrayList.this.elementData[offset + index] = e; return oldValue; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } public int size() { checkForComodification(); return this.size; } public void add(int index, E e) { rangeCheckForAdd(index); checkForComodification(); parent.add(parentOffset + index, e); this.modCount = parent.modCount; this.size++; } .... } |
3.[强制] 在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生ConcurrentModificationException异常。
老四附言:
关于这个ConcurrentModificationException异常老四曾经也是采坑过来的,所以早就有前辈指点江山,比如这篇文章就讲的很详细,请戳《Java ConcurrentModificationException异常原因和解决方法》仔细阅读品味,收获颇丰。
4.[强制] 使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是list.size()。
说明: 使用toArray带参方法,入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址:如果数组元素个数大于实际所需,下标为[list.size()]的数组元素将被置为null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
正例:
1 2 3 4 5 |
List<String> list = new ArrayList<String>(2); list.add("guan"); list.add("bao"); String[] array = new String[list.size()]; array = list.toArray(array); |
反例: 直接使用toArray无参方法存在问题,此方法返回值只能是Object[]类,若强转其它类型数组将出现ClassCastException错误。
老四附言:
解释的已经很清楚了,我还多哔哔啥。
5.[强制] 使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。
说明: asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
1 2 |
String[] str = new String[] { "you", "wu" }; List list = Arrays.asList(str); |
第一种情况: list.add(“yangguanbao”); // 运行时异常。
第二种情况: str[0] = “gujin”; // 那么list.get(0)也会随之修改。
老四附言:
关于适配器模式您可以参考老四写的《浅析设计模式第十七章之适配器模式》。接下来我们可以参考一下asList源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
@SafeVarargs @SuppressWarnings("varargs") public static <T> List<T> asList(T... a) { return new ArrayList<>(a); } private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable { private static final long serialVersionUID = -2764017481108945198L; private final E[] a; ArrayList(E[] array) { a = Objects.requireNonNull(array); } @Override public int size() { return a.length; } @Override public Object[] toArray() { return a.clone(); } @Override @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { int size = size(); if (a.length < size) return Arrays.copyOf(this.a, size, (Class<? extends T[]>) a.getClass()); System.arraycopy(this.a, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } @Override public E get(int index) { return a[index]; } @Override public E set(int index, E element) { E oldValue = a[index]; a[index] = element; return oldValue; } @Override public int indexOf(Object o) { E[] a = this.a; if (o == null) { for (int i = 0; i < a.length; i++) if (a[i] == null) return i; } else { for (int i = 0; i < a.length; i++) if (o.equals(a[i])) return i; } return -1; } @Override public boolean contains(Object o) { return indexOf(o) != -1; } @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(a, Spliterator.ORDERED); } @Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); for (E e : a) { action.accept(e); } } @Override public void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); E[] a = this.a; for (int i = 0; i < a.length; i++) { a[i] = operator.apply(a[i]); } } @Override public void sort(Comparator<? super E> c) { Arrays.sort(a, c); } } |
可以看到,asList返回一个新的arrayList,但是是arrays的内部类,在内部类中新声明的关于list操作方法,但是具体实现都是用数组来实现的。
6.[强制] 泛型通配符<? extends T >来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作为接口调用赋值时易出错。
说明: 扩展说一下PECS(Producer Extends Consumer Super)原则:
- 频繁往外读取内容的,适合用<? extends T >。
- 经常往里插入的,适合用 <? super T>。
老四附言:
本条规则可能很多人都没有听说或者了解过,我们从开始一点点的来了解。首先我们先来复习一下关于泛型、extends、super等的一些基本知识。
我们都知道extends、super关键字在Java中是用来表示继承的,子类继承父类。继承是面向对象的三大特征之一,也是实现软件复用的重要手段,关于Java中的单继承可能不需要老四哔哔太多,简单写几点当作复习和注意点就好了:
- extends的实际意思是拓展而不是继承,子类对父类的拓展,子类是特殊的父类。
- Java的子类不能获得父类的构造器
- Java类只能有一个直接父类,实际上,Java类可以有无线多个间接父类
- Object类是所有类的父类,要么是直接的父类,要么是间接地父类
- 子类包含与父类同名方法的现象被称为方法重写,重写与重载不应该放在一起来比较
- super可以用来在子类中调用父类中被覆盖(重写,Override)的方法
- super用于限定该对象调用它从父类继承得到的实例变量或方法,通this一样,不能出现在static修饰的方法中
- 在构造器中,在子类构造器中调用父类构造器使用super调用来完成,super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
- 同this很像,不过super是在子类中调用父类的构造器,所以也必须出现在子类构造器执行体的第一行,super与this不会同时出现。
接下来我们需要重点来复习甚至是重新认识一下Java中的泛型知识。为此,老四专门写了一篇文章浅析,在里面也阐述了PECS原则。详情可以戳《浅析Java中的泛型以及泛型中的PECS(Producer Extends,Consumer Super)原则》参考一下。
7.[强制] 不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
正例:
1 2 3 4 5 6 7 |
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (删除元素的条件) { iterator.remove(); } } |
反例:
1 2 3 4 5 6 7 8 |
List<String> list = new ArrayList<String>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.remove(item); } } |
说明: 以上代码的执行结果肯定会出乎大家的意料,那么试一下把”1″换成”2″,会是同样的结果吗?
老四附言:
首先我们来验证一下说明里面的情况,看看把1换成2两者之间分别是什么样的结果:
验证之后,我们又看到了熟悉java.util.ConcurrentModificationException(并发同时修改异常)异常,请参考第三条的老四附言浅析。所以在foreach中我们尽量不要进行remove等元素操作,如果需要,请使用Iterator。
8.[强制] 在JDK7版本及以上,Comparator要满足如下三个条件,不然Arrays.sort,Collections.sort会报IllegalArgumentException异常。
说明: 三个条件如下:
- x,y的比较结果和y,x的比较结果相反。
- x > y,y > z,则x > z。
- x = y,则x,z比较结果和y,z比较结果相同。
反例: 下例中没有处理相等的情况,实际使用中可能会出现异常:
1 2 3 4 5 6 |
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; } }; |
老四附言:
JDK7中的Collections.sort方法实现中,如果两个两个值相等,compare()方法需要返回0才行,要不然的话就会报错。这里面涉及到一个排序算法,叫做TimSort的算法,这个算法是一种优化类型的归并排序算法,之前老四在浅析归并排序的时候也提及到过归并排序的使用场景就是举得这个例子,可以简单参考批评指导一下老四写的《浅析数据结构排序篇之归并排序Merge sort》这篇文章。至于这个TimSort排序算法以及引起这个问题的原因可以参考如下几篇文章,写的还是比较清晰明了的,老四也就不再这里哔哔哔了。
- 《jdk7 Collections.sort()引发的IllegalArgumentException》
- 《OpenJDK 源代码阅读之 TimSort》
- 《[译]理解timsort, 第一部分:适应性归并排序(Adaptive Mergesort)》
9.[推荐] 集合初始化时,指定集合初始值大小。
说明: HashMap使用HashMap(int initialCapacity)初始化。
正例: initialCapacity=(需要存储的元素个数/负载因子)+1。注意负载因子(即loaderfactor)默认为0.75,如果暂时无法确定初始值大小,请设置为16(即默认值)。
反例: HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,resize需要重建hash表,严重影响性能。
老四附言:
老四之前文章《阿里巴巴Java开发规约第一章-并发处理篇 吐血浅析》第14条规约中浅析过关于HashMap的一些底层内容,相关知识可以前去参考批评指导一下,抱拳。
10.[推荐] 使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
说明: keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把 key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。
正例: values()返回的是V值集合,是一个list集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。
老四附言:
这个没什么可说的,有理有据,按照规范执行就好,这里简单的示例一下Java中的关于Map.foreach的方法的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.glorze.map; import java.util.HashMap; import java.util.Map; /** * 集合的forEach操作 * @ClassName MapTest * @author: 高老四 * @since: 2018年9月29日 下午5:00:15 */ public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(16); map.put("A", "高老四博客"); map.put("B", "glorze.com"); map.put("C", "http://www.glorze.com"); map.put("D", "倚楼听风雨"); map.put("E", "淡看江湖路"); String magic = "E"; map.forEach((k,v)->System.out.println("key: " + k + " value: " + v)); map.forEach((k,v)->{ System.out.println("key: " + k + " value: " + v); if(magic.equals(k)){ System.out.println("结束forEach"); } }); } } |
11.[推荐] 高度注意Map类集合K/V能不能存储null值的情况,如下表格:
集合类
|
Key
|
Value
|
Super
|
说明
|
HashTable
|
不允许为null
|
不允许为null
|
Dictionary
|
线程安全
|
ncurrentHashMap
|
不允许为null
|
不允许为null
|
AbstractMap
|
锁分段技术(JDK8:CAS,compare and swap,
比较并交换,原子操作的一种,多线程中实现不被打断的数据
交换操作)
|
TreeMap
|
不允许为null
|
允许为null
|
AbstractMap
|
线程不安全
|
HashMap
|
允许为null
|
允许为null
|
AbstractMap
|
线程不安全
|
反例: 由于HashMap的干扰,很多人认为ConcurrentHashMap是可以置入null值,而事实上,存储null值时会抛出NPE异常。
老四附言:
emmmmmmmmmmmmm~~~~~~~~~~,不进是Map,空指针一直都是Java中的大问题,老四前阶段参加GDD(谷歌开发者大会)的时候,会上就讲过,安卓方面谷歌已经将Kotlin语言作为官方承认的语言,并且与Java兼容且很好的处理关于空指针的问题。
12.[参考] 合理利用好集合的有序性(sort)和稳定性(order)避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明: 有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如: ArrayList是order/unsort;HashMap是unorder/unsort;TreeSet是order/sort。
老四附言:
没啥说的了,写累了。
13.[参考] 利用Set元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List的contains方法进行遍历、对比、去重操作。
老四附言:
这个规约老四是不相信真的有人会这么做…………..
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额,老四这里抱拳了。赞助时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的赞赏钱财也会被用于小站的服务器运维上面,再次抱拳。