ArrayList的contains到了十万级别,就基本上很慢了,HashSet到了百万级的数据也特别快,而且是快得特别多。
ArrayList的contains方法与HashSet的contains方法的区别?
ArrayList的contains方法会直接判断元素的内存地址是否相同,HashSet的contains方法会先判断元素的hashCode是否相同,如果不同则返回false,如果相同再用equals判断,因此HashSet的contains方法的方法效率会更高。
ArrayList.contains()
ArrayList的contains()方法会调用其
indexOf()
方法,在indexOf()
方法里边,有一个for循环。所以,ArrayList的contains()方法的
时间复杂度是O(n)
该方法通过遍历数据和比较元素的方式来判断是否存在给定元素。当ArrayList中存放的元素非常多时,这种实现方式来判断效率将非常低。
public boolean contains(Object o) { return indexOf(o) >= 0; } 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])) return i; } return -1; }
HashSet.contains()
既然ArrayList的contains()方法存在性能问题,那么就应该寻找改进的办法。这里推荐使用HashSet来代替ArrayList。
下面介绍HashSet的contains()方法的实现过程:
注:HashSet将元素存放在HashMap中(HashMap的key)
public boolean contains(Object o) { return map.containsKey(o); }
contains()方法调用HashMap的containsKey()方法
/** * 检查是否包含key * 如果key有对应的节点对象,则返回ture,不关心节点对象的值是否为空 */ public boolean containsKey(Object key) { // 调用getNode方法来获取键值对,如果没有找到返回false,找到了就返回ture return getNode(hash(key), key) != null; //真正的查找过程都是通过getNode方法实现的 }
containsKey()方法调用getNode()方法。在该方法中,首先根据key计算hash值,然后从HashMap中取出该hash值对应的链表(链表的元素个数将很少),再通过变量该链表判断是否存在给定值。这种实现方式效率将比ArrayList的实现方法效率高非常多。
/** * 该方法是Map.get方法的具体实现 * 接收两个参数 * @param hash key的hash值,根据hash值在节点数组中寻址,该hash值是通过hash(key)得到的,可参见:hash方法解析 * @param key key对象,当存在hash碰撞时,要逐个比对是否相等 * @return 查找到则返回键值对节点对象,否则返回null */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 声明节点数组对象、链表的第一个节点对象、循环遍历时的当前节点对象、数组长度、节点的键对象 // 节点数组赋值、数组长度赋值、通过位运算得到求模结果确定链表的首节点 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // 首先比对首节点,如果首节点的hash值和key的hash值相同 并且 首节点的键对象和key相同(地址相同或equals相等),则返回该节点 ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 返回首节点 // 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着key没有匹配的键值对 if ((e = first.next) != null) { // 如果存在下一个节点 e,那么先看看这个首节点是否是个树节点 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)))) // 比对时还是先看hash值是否相同、再看地址或equals return e; // 如果当前节点e的键对象和key相同,那么返回e } while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环 } } return null; // 在比对完了应该比对的树节点 或者全部的链表节点 都没能匹配到key,那么就返回null }
实例验证
测试ArrayList
public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); // 存入100000个数据 for (int i = 0; i < 100000; i++) { arrayList.add("test" + i); } // 验证300000个数据(其中200000不存在) long beginTime = System.currentTimeMillis(); for (int i = 0; i < 300000; i++) { arrayList.contains("test" + i); } long endTime = System.currentTimeMillis(); System.out.println("cost time: " + (endTime - beginTime) + "ms"); }
打印结果:
cost time: 182210ms测试HashSet
public static void main(String[] args) { Set<String> hashSet = new HashSet<>(); // 存入100000个数据 for (int i = 0; i < 100000; i++) { hashSet.add("test" + i); } // 验证300000个数据(其中200000不存在) long beginTime = System.currentTimeMillis(); for (int i = 0; i < 300000; i++) { hashSet.contains("test" + i); } long endTime = System.currentTimeMillis(); System.out.println("cost time: " + (endTime - beginTime) + "ms"); }
打印结果:
cost time: 49ms总结
通过以上实例可以看出,使用ArrayList的contains()耗时是使用HashSet的contains()方法的3000多倍。