hashCode()方法以及集合中Set的一些总结
一、前言
本篇文章没有什么主题,就是一些零散点的总结。
周末没事看了几道蚂蚁金服的面试题,其中有好几道都是特别简单的,基础性的题目,就是我们平时用到的,但是发现要是完全说出来还是有一些不清楚的地方,所以小小的总结一下。
二、hashCode()方法理解
提到hashCode()必然会涉及equals()方法,二者是紧密相连的,其实面试中被问到这方面往往是考察集合存储对象判断相等的问题。
比如有如下Person类:
1public class Person {
2
3 private int age;
4 private String name;
5
6 public Person(int age, String name) {
7 this.age = age;
8 this.name = name;
9 }
10
11 public int getAge() {
12 return age;
13 }
14
15 public void setAge(int age) {
16 this.age = age;
17 }
18
19 public String getName() {
20 return name;
21 }
22
23 public void setName(String name) {
24 this.name = name;
25 }
26
27 @Override
28 public boolean equals(Object o) {
29 if (this == o) return true;
30 if (o == null || getClass() != o.getClass()) return false;
31 Person person = (Person) o;
32 return age == person.age &&
33 Objects.equals(name, person.name);
34 }
35}
很简单吧,我这里只重写了equals方法,如果我们以Person类对象作为key存储在HashMap中,如下:
1 HashMap map = new HashMap();
2 map.put(new Person(45,"lisi"),"123");
3 System.out.println(map.get(new Person(45,"lisi")));
试想一下能正常取出"lisi"值吗?对HashMap源码看过的同学肯定知道取不出来,打印如下:
1 null
HashMap在取数据的时候会检查对应的key是否已经存储过,这个比较简单来说就是比较key的hashcode()值以及equals()是否相等的比较,只有二者均相同才会认为已经存储过,对于上述Person类我们只重写了equals方法,对于hashcode()方法默认调用的是Object类中的hashcode()方法:
1 public int hashCode() {
2 return identityHashCode(this);
3 }
不同对象会生成不同的hash值,所以严格来说hashcode()与equals()方法我们最好同时重写,否则与集合类结合使用的时候会产生问题,改造Person类添加如下hashcode()方法:
1 @Override
2 public int hashCode() {
3 return Objects.hash(age, name);//根据类中属性生成对应hash值
4 }
这样就可以正常获取对应值了。
HashMap中比较元素是否相同是根据Key的hashcode值以及equals来判断是否相同的。
三、Set集合常用类相关问题
Set集合常用与存储不重复的数据,也就是集合中数据都不相等,但是不同具体实现类判断是否相等是不一样,这也是面试中会问到的问题,比如TreeSet是怎么判断元素是否相同的?HashSet是怎么判断的?
其实稍微看一下源码就明白了,Set具体实现类都是依靠对应map来实现的:
- HashSet底层依靠HashMap来实现
- TreeSet底层依靠TreeMap来实现
- LinkedHashSet底层依靠LinkedHashMap来实现
HashSet
看一下HashSet源码吧:
1public class HashSet<E>
2 extends AbstractSet<E>
3 implements Set<E>, Cloneable, java.io.Serializable
4{
5 static final long serialVersionUID = -5024744406713321676L;
6
7 private transient HashMap<E,Object> map;
8
9 // Dummy value to associate with an Object in the backing Map
10 private static final Object PRESENT = new Object();
11
12 public HashSet() {
13 map = new HashMap<>();
14 }
15
16 public HashSet(Collection<? extends E> c) {
17 map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
18 addAll(c);
19 }
20
21 public HashSet(int initialCapacity, float loadFactor) {
22 map = new HashMap<>(initialCapacity, loadFactor);
23 }
24
25 public HashSet(int initialCapacity) {
26 map = new HashMap<>(initialCapacity);
27 }
28
29 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
30 map = new LinkedHashMap<>(initialCapacity, loadFactor);
31 }
32
33 public Iterator<E> iterator() {
34 return map.keySet().iterator();
35 }
36
37 public int size() {
38 return map.size();
39 }
40
41 public boolean isEmpty() {
42 return map.isEmpty();
43 }
44
45 public boolean contains(Object o) {
46 return map.containsKey(o);
47 }
48
49 public boolean add(E e) {
50 return map.put(e, PRESENT)==null;
51 }
52
53 public boolean remove(Object o) {
54 return map.remove(o)==PRESENT;
55 }
56
57 public void clear() {
58 map.clear();
59 }
这里只列出了部分方法,不过已经足够了,几个构造方法也就是初始化HashMap,其余的方法也都是调用HashMap对应的方法,所以你要是理解了HashMap那HashSet几秒钟就全都懂了,不理解HashMap请转到:
Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析
我们在调用add(E e)方法的时候,key就是e,而value永远是PRESENT,也就是Object()对象了。
这里注意一下:
1 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
2 map = new LinkedHashMap<>(initialCapacity, loadFactor);
3 }
这个构造方法是给LinkedHashSet调用的,我们无法使用,没有public修饰。
所以要是问你HashSet如何判断元素重复的,也就是和HashMap一样通过hashcode()与equals()方法来判断。
LinkedHashSet
接下来看下LinkedHashSet源码:
1public class LinkedHashSet<E>
2 extends HashSet<E>
3 implements Set<E>, Cloneable, java.io.Serializable {
4
5 private static final long serialVersionUID = -2851667679971038690L;
6
7 public LinkedHashSet(int initialCapacity, float loadFactor) {
8 super(initialCapacity, loadFactor, true);
9 }
10
11 public LinkedHashSet(int initialCapacity) {
12 super(initialCapacity, .75f, true);
13 }
14
15 public LinkedHashSet() {
16 super(16, .75f, true);
17 }
18
19 public LinkedHashSet(Collection<? extends E> c) {
20 super(Math.max(2*c.size(), 11), .75f, true);
21 addAll(c);
22 }
23
24 @Override
25 public Spliterator<E> spliterator() {
26 return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
27 }
28}
就是那么简短,LinkedHashSet继承HashSet,初始化调用的就是HashSet中三个参数的构造函数,上面已经提到,也就map初始为LinkedHashMap,如果你对LinkedHashMap完全理解,那么这里就十分简单了,如果不理解LinkedHashMap,请转到:
Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析
总结一句话:LinkedHashSet是数据无重复并基于存入数据顺序排序的集合,数据重复判断的依据依然是hashcode()与equals()方法来判断。
TreeMap
同样看一下TreeMap中部分源码,构造部分:
1 private transient NavigableMap<E,Object> m;
2
3 private static final Object PRESENT = new Object();
4
5 TreeSet(NavigableMap<E,Object> m) {
6 this.m = m;
7 }
8
9 public TreeSet() {
10 this(new TreeMap<E,Object>());
11 }
12
13 public TreeSet(Comparator<? super E> comparator) {
14 this(new TreeMap<>(comparator));
15 }
看到了吧,构造时我们可以自己指定一个NavigableMap,如不指定则默认为TreeMap,所以TreeSet底层实现为TreeMap,加入数据的时候value同样永远是Object:
1 public boolean add(E e) {
2 return m.put(e, PRESENT)==null;//private static final Object PRESENT = new Object();
3 }
TreeMap如不熟悉请转到:
TreeMap是怎么比较数据是否相等的呢?怎么排序的呢?这里我们就要查看一下TreeMap中的put方法源码了:
1 public V put(K key, V value) {
2 TreeMapEntry<K,V> t = root;
3 if (t == null) {
4 // BEGIN Android-changed: Work around buggy comparators. http://b/34084348
5 // We could just call compare(key, key) for its side effect of checking the type and
6 // nullness of the input key. However, several applications seem to have written comparators
7 // that only expect to be called on elements that aren't equal to each other (after
8 // making assumptions about the domain of the map). Clearly, such comparators are bogus
9 // because get() would never work, but TreeSets are frequently used for sorting a set
10 // of distinct elements.
11 //
12 // As a temporary work around, we perform the null & instanceof checks by hand so that
13 // we can guarantee that elements are never compared against themselves.
14 //
15 // **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****
16 //
17 // Upstream code was:
18 // compare(key, key); // type (and possibly null) check
19 if (comparator != null) {
20 if (key == null) {
21 comparator.compare(key, key);
22 }
23 } else {
24 if (key == null) {
25 throw new NullPointerException("key == null");
26 } else if (!(key instanceof Comparable)) {
27 throw new ClassCastException(
28 "Cannot cast" + key.getClass().getName() + " to Comparable.");
29 }
30 }
31 // END Android-changed: Work around buggy comparators. http://b/34084348
32 root = new TreeMapEntry<>(key, value, null);
33 size = 1;
34 modCount++;
35 return null;
36 }
37 int cmp;
38 TreeMapEntry<K,V> parent;
39 // split comparator and comparable paths
40 Comparator<? super K> cpr = comparator;
41 if (cpr != null) {
42 do {
43 parent = t;
44 cmp = cpr.compare(key, t.key);
45 if (cmp < 0)
46 t = t.left;
47 else if (cmp > 0)
48 t = t.right;
49 else
50 return t.setValue(value);
51 } while (t != null);
52 }
53 else {
54 if (key == null)
55 throw new NullPointerException();
56 @SuppressWarnings("unchecked")
57 Comparable<? super K> k = (Comparable<? super K>) key;
58 do {
59 parent = t;
60 cmp = k.compareTo(t.key);
61 if (cmp < 0)
62 t = t.left;
63 else if (cmp > 0)
64 t = t.right;
65 else
66 return t.setValue(value);
67 } while (t != null);
68 }
69 TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
70 if (cmp < 0)
71 parent.left = e;
72 else
73 parent.right = e;
74 fixAfterInsertion(e);
75 size++;
76 modCount++;
77 return null;
78 }
通过查看put方法逻辑,初始化TreeMap可以自己指定比较器comparator,如果我们指定了comparator那么数据的比较优先使用指定的comparator,是否存入null也由我们自己的比较器comparator决定,如果没指定那么存入的元素必须实现Comparable接口,否则抛出异常。
回到TreeSet,也就是TreeSet比较元素是否相等时如果我们指定了comparator那么就根据其compare方法返回值来比较,0代表相等,如果没指定那么就需要数据自己实现Comparable接口,是否相等根据compareTo返回值决定,0代表相等。
TreeSet也能保证数据的有序性,与LinkedHashSet基于插入顺序排序不同,TreeSet排序是根据元素比较来排序的。
蚂蚁金服有道面试题是:TreeSet存入数据有什么要求?看完上面你知道怎么回答了吗?很简单,如果我们没指定TreeSet集合的比较器那么插入的数据需要实现Comparable接口用来比较元素是否相等以及排序用。
好了,以上就是Set集合的一些总结。
四、HashMap线程不安全的体现
fail-fast机制
我们知道大部分集合类中在用迭代器迭代过程中要删除集合中元素最好用迭代器的删除方法,否则会发生并发异常,如下:
1 HashMap map = new HashMap();
2 map.put(1,"1");
3 map.put(2,"2");
4 map.put(3,"3");
5 map.put(4,"4");
6
7 Set entrySet = map.entrySet();
8 Iterator<Map.Entry> iterator = entrySet.iterator();
9 while (iterator.hasNext()){
10 Map.Entry entry = iterator.next();
11 if (entry.getKey().equals(2)){
12 map.remove(entry.getKey());//调用集合类本身的删除方法
13 }
14 System.out.println(entry.getKey()+"--->"+entry.getValue());
15 }
运行程序如下:
1 1--->1
2 2--->2
3 Exception in thread "main" java.util.ConcurrentModificationException
4 at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
5 at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
6 at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
7 at com.wanglei55.mjavalib.myClass.main(myClass.java:173)
至于产生问题原因这里我就不细说了,很基础的。
改为如下用迭代器删除就可以了:
1 HashMap map = new HashMap();
2 map.put(1,"1");
3 map.put(2,"2");
4 map.put(3,"3");
5 map.put(4,"4");
6
7 Set entrySet = map.entrySet();
8 Iterator<Map.Entry> iterator = entrySet.iterator();
9 while (iterator.hasNext()){
10 Map.Entry entry = iterator.next();
11 if (entry.getKey().equals(2)){
12 iterator.remove();//改为迭代器删除
13 }
14 System.out.println(entry.getKey()+"--->"+entry.getValue());
15 }
这里我们看一下迭代器怎么删除的:
1 public final void remove() {
2 Node<K,V> p = current;
3 if (p == null)
4 throw new IllegalStateException();
5 if (modCount != expectedModCount)
6 throw new ConcurrentModificationException();
7 current = null;
8 K key = p.key;
9 // 迭代器调用的也是集合本身的删除方法核心逻辑,我们知道集合本身删除会改变modCount值,但是迭代器删除后紧接着重新赋值expectedModCount = modCount,这样就不会产生并发异常
10 removeNode(hash(key), key, null, false, false);
11 expectedModCount = modCount;
12 }
上面是在单线程下,如果多线程呢?我们看一下,改造代码如下:
1final HashMap map = new HashMap();
2 map.put(1,"1");
3 map.put(2,"2");
4 map.put(3,"3");
5 map.put(4,"4");
6
7 Thread t1 = new Thread(){
8 @Override
9 public void run() {
10 Set entrySet = map.entrySet();
11 Iterator<Map.Entry> iterator = entrySet.iterator();
12 while (iterator.hasNext()){
13 Map.Entry entry = iterator.next();
14 if (entry.getKey().equals(2)){
15 iterator.remove();//改为迭代器删除
16 }
17 }
18 }
19 };
20
21 Thread t2 = new Thread(){
22 @Override
23 public void run() {
24 Set entrySet = map.entrySet();
25 Iterator<Map.Entry> iterator = entrySet.iterator();
26 while (iterator.hasNext()){
27 Map.Entry entry = iterator.next();
28 System.out.println(entry.getKey()+"--->"+entry.getValue());
29 try {
30 Thread.sleep(500);
31 } catch (InterruptedException e) {
32 e.printStackTrace();
33 }
34 }
35 }
36 };
37
38 t1.start();
39 t2.start();
运行程序,有时依然会报并发异常:
1 1--->1
2 Exception in thread "Thread-1" java.util.ConcurrentModificationException
3 at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
4 at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
5 at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
6 at com.wanglei55.mjavalib.myClass$2.run(myClass.java:191)
上面我们在每个线程都获取了迭代器: Iterator iterator = entrySet.iterator();我们看下获取迭代器的源码:
1 public final Iterator<Map.Entry<K,V>> iterator() {
2 return new EntryIterator();
3 }
直接返回一个新的迭代器,也就是说每个线程有自己的迭代器,初始化的时候各自的expectedModCount等于modCount,当一个线程调用remove()方法后会改变共用的modCount,而另一个线程的expectedModCount依然等于原先的modCount,这样另一个线程在进行迭代操作的时候就会发生并发异常。
那怎么解决呢?有同学估计会想到用Hashtable啊,Hashtable是线程安全的,其实你改造上述代码为Hashtable也同样会发生并发异常,Hashtable线程安全是指的put,get这些方法是线程安全的,而这里的问题是每个线程有自己的迭代器,我们需要给迭代过程加锁,如下:
1 final HashMap map = new HashMap();
2
3 for (int i = 0; i < 20; i++) {
4 map.put(i,i);
5 }
6
7 Thread t1 = new Thread(){
8 @Override
9 public void run() {
10 synchronized (myClass.class){
11 Set entrySet = map.entrySet();
12 Iterator<Map.Entry> iterator = entrySet.iterator();
13 while (iterator.hasNext()){
14 Map.Entry entry = iterator.next();
15 if (entry.getKey().equals(2)){
16 iterator.remove();//改为迭代器删除
17 }
18 }
19 }
20 }
21 };
22
23 Thread t2 = new Thread(){
24 @Override
25 public void run() {
26 synchronized (myClass.class){
27 Set entrySet = map.entrySet();
28 Iterator<Map.Entry> iterator = entrySet.iterator();
29 while (iterator.hasNext()){
30 Map.Entry entry = iterator.next();
31 System.out.println(entry.getKey()+"--->"+entry.getValue());
32 try {
33 Thread.sleep(500);
34 } catch (InterruptedException e) {
35 e.printStackTrace();
36 }
37 }
38 }
39 }
40 };
41
42 t2.start();
43 t1.start();
或者改为支持并发的ConcurrentHashMap,这样就可以解决并发问题了。
hashCode()方法以及集合中Set的一些总结的更多相关文章
- 【转载】C#中List集合使用Reverse方法对集合中的元素进行倒序反转
在C#的List集合操作中,有时候需要对List集合中的元素的顺序进行倒序反转操作,此时就可使用到List集合中的Reverse方法来实现此功能,Reverse方法的签名为void Reverse() ...
- Java中hashCode()方法以及HashMap()中hash()方法
Java的Object类中有一个hashCode()方法: public final native Class<?> getClass(); public native int hashC ...
- c# Array或List有个很实用的ForEach方法,可以直接传入一个方法对集合中元素操作
using System; using System.Collections.Generic; namespace demo { class Program { static void Main(st ...
- java集合(3)- Java中的equals和hashCode方法详解
参考:http://blog.csdn.net/jiangwei0910410003/article/details/22739953 Java中的equals方法和hashCode方法是Object ...
- Java中的equals和hashCode方法
本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要 ...
- 集合中list、ArrayList、LinkedList、Vector的区别、Collection接口的共性方法以及数据结构的总结
List (链表|线性表) 特点: 接口,可存放重复元素,元素存取是有序的,允许在指定位置插入元素,并通过索引来访问元素 1.创建一个用指定可视行数初始化的新滚动列表.默认情况下,不允许进行多项选择. ...
- Java中的equals和hashCode方法详解
Java中的equals和hashCode方法详解 转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...
- 转:Java中的equals和hashCode方法详解
转自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这 ...
- Day07_37_深度剖析集合中的contains()方法
深度剖析集合中的 contains()方法 contains()方法查找集合中是否包含某个元素 contains() 底层使用的是 equals()方法 当contains()方法拿到一个对象的时候, ...
随机推荐
- Python库
--Python库之Pandas库-------- 自主选择学习了Python中的Pandas库,以下是本人对Pandas库的认识: Pandas库是Python最受欢迎的库之一,主要用于数据的操作. ...
- Python_pickle模块操作二进制文件
import pickle b=7 i=13000000 fa=99.056 s='中国人民 123abc' lst=[[1,2,3],[4,5,6],[7,8,9]] tu=(-5,10,8) co ...
- Spring Boot实战笔记(五)-- Spring高级话题(Spring Aware)
一.Spring Aware Spring 依赖注入的最大亮点就是你所有的 Bean 对 Spring容器的存在是没有意识的.即你可以将你的容器替换成其他的容器,如Google Guice,这时 Be ...
- Shell脚本中获取select值
最近做一个数据清理,根据行号清理,所以需要查出这个行的最大最小值出来进行删除,如果靠手动每次去查,太麻烦所以就用在sh脚本当中执行SELECT语句,并将结果赋值给一个变量. sh脚本如下 #! /bi ...
- virsh命令来创建虚拟机
virsh命令来创建虚拟机步骤 (1)生成硬盘镜像文件: 格式:raw或qcow2 # qemu-img create -f raw fdisk.img 10G qemu-img convert re ...
- 利用AOP实现SqlSugar自动事务
先看一下效果,带接口层的三层架构: BL层: public class StudentBL : IStudentService { private ILogger mLogger; private r ...
- SSM-SpringMVC-24:SpringMVC异常高级之自定义异常
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 自定义异常,大家都会,对吧,无非就是继承异常类等操作,很简单,我就不多扯皮了,但是在xml配置文件中有个不同的 ...
- ZooKeeper的使用---命令端
一.进入命令行 ./bin/zkCli.sh 二.常用命令 命令 作用 范例 备注 connect host:port 连接其他zookeeper客户端 connect hadoop2:21 ...
- CentOS 安装Python3.x常见问题
CentOS 6.x自带的Python版本是2.6,CentOS 7.x上自带的是2.7,我们要自己安装Python3.X,配置环境,不过一般安装过程不会一帆风顺,往往有些报错,在CentOS以及其他 ...
- 部署:持续集成(CI)与持续交付(CD)——《微服务设计》读书笔记
系列文章目录: <微服务设计>读书笔记大纲 一.CI(Continuous Integration)简介 CI规则1:尽量频繁地把代码签入到分支中以进行集成 CI规则2: ...