map的key就是一个set,而且map提供了一个返回key集合:Set<k> ketset()java
而从set到map,set中的每一个元素都是k-v对便可算法
因为给出key,map总能够找到对应的value,因此能够将value当作key的附属物数组
HashSet与HashMap性能
两个都是经过Hash算法来计算集合元素的存储位置this
集合存储Java对象并非指将java对象放在集合中,而是集中保留了这些对象的引用(指针),这些引用变量指向实际的java对象spa
能够看出,list中存储了两个对象,这个两个对象最终仍是指向了堆内存中的实际java对象,即说明list中存储的是对象的引用debug
它是Map的一个内部接口,每一个Map.Entry实际就是一个k-v对,而且系统在存储k-v对时,没有考虑v,而是仅仅根据k来计算Entry的位置,位置肯定以后,value也随之保存了指针
当调用put方法时,即试图将一个k-v对放入一个HashMap时,首先根据key的hashCode()返回值决定Entry的存储位置,若是两个Entry的key的hashCode值相同,那么他们对应的v就被新的v覆盖,k不会覆盖,若是不一样,则新添加的Entry将于原有的Entry造成Entry链,而且新添加的Entry位于Entry链的头部code
public HashMap(int initialCapacity, float loadFactor) { //初始容量不能为负数 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始容量大于最大容量时,将初始容量赋值为最大容量 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //计算出大于initCapacity的最小的2的n次方幂 /*当实际容量小于初始容量时,使用位移运算,将实际容量不断的乘以2(最终的效果就是乘以2的n次方),直至实际容量大于初始容量*/ // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; //设置容量极限等于容量乘以负载因子 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); //初始化table数组 table = new Entry[capacity]; init(); }
通过debug发现初始化数组的时候,居然也会调用上面这个HashMap的构造方法…对象
查看源码发现
HashMap的底层经过数组来存储数据的,该数组的长度不必定是指定的初始长度,而是比初始值大的最小的2的n次方,因此尽可能使初始值是2的n次方,这样能够减少系统开销。
HashMap及其子类采用Hash算法来决定集合中元素的位置,系统初始化HashMap时,系统会建立一个长度为capacity的数组Entry,该数组里能够存储元素的位置被称为桶(bucket),每一个bucket都有其指定的索引,经过索引,系统能够快速的访问到bucket里存储的元素
每一个bucket只能存储一个元素,即一个Entry,可是因为Entry对象能够包含一个引用变量用于指向下一个Entry,因此可能出现bucket中只有一个Entry,但这个Entry指向另外一个Entry,造成Entry链。
若是bucket只有一个Entry,系统先计算某个key的hashCode值,在根据hashCode值找到该key在table数组中的索引,而后取出该索引处的Entry,最后返回该key对应的value
若是bucket存储的是一个Entry链,那么找到该Entry以后还要遍历该Entry链才能找到特定的Entry
源码
public V get(Object key) { //若是key是null,调用getForNullKey取出value if (key == null) return getForNullKey(); //计算key的hashCode int hash = hash(key.hashCode()); //直接取出table数组中指定索引处的值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; //搜索该Entry链的下一个entry e = e.next) { Object k; //若是该Entry的key和被搜索的key相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
建立HashMap时,有一个默认的负载因子(load factor),默认为0.75,增大负载因子能够减小hash表所占的空间,但会增长查询时间,而最频繁的操做put和get都须要用到查询;反之,减少负载因子会提升查询效率可是会下降存储空间
查看HashSet源码
它只是封装了一个HashMap,它所存储的元素其实是由HashMap的key来保存的,而该HashMap的value则是一个PRESENT,一个静态的java对象
HashMap和HashSet判断重复的标准包括两个对象的hashCode值相等,而且二者equals~!若是要判断本身的类对象是否重复,也须要重写hashCode和equals~!
例如:
若是不重写hashCode则不能正确判断两个对象是否相等
TreeSet底层实际上也是有TreeMap实现的
对于TreeMap,它采用红黑树的排序二叉树来保存Map中每一个Entry—每一个Entry都被当作红黑树的一个节点来对待。
当向TreeMap中存入数据的时候,首先会把第一个元素的Entry做为根节点,后面添加的元素的Entry都做为新节点添加到已知的红黑树中,这样就保证了TreeMap中的key按照由小到大的顺序排列。(该二叉树自己就有默认的排序)
TreeMap和HashMap相比,性能较低,由于在查询和插入元素的时候须要不断的循环,找到合适Entry(的位置),可是它优点是有序。
关于红黑树:
TreeMap的关键就是put(K key,V vlaue), 该方法实现将Entry放入到TreeMap的Entry链。
源码:
分析源代码:
首先该map若是为空,即没有任何元素,t==null,那么就建立一个新的Entry,并将该Entry做为root;
接着,就要去寻找合适的put的位置了,经过判断用户是否认制了Comparator,若是有定制,则使用定制的排序方式,不然就使用默认的(新建的)Comparaotor,该比较器的做用是找到合适的put的节点,并把该节点做为即将被put的元素的父亲节点;
找寻的方式是:首先将根节点做为父亲节点,而后经过Compaerator判断该父亲节点的key与被put的key的大小,若是父亲节点大,那么就将父亲节点的左节点做为新的父亲节点;反之则将父亲节点的右节点做为新的父亲节点;若是相等,则覆盖原有节点上的value,并经过return返回,再也不循环;
一轮循环结束后再去判断新的父亲节点是否真实存在,若是存在,则继续循环遍历,不然退出循环。
最终的结果就是,找到了该TreeMap中合适位置的节点
最后将循环找到的节点做为新添加节点的父节点,再次去判读父节点的key和新添加节点的key的大小,父亲节点大,则添加在父亲节点的左节点,反之则在右节点(不存在相等了)。
使用get方法查找元素时:
最终调用的仍是getEntry()方法:
能够看出,getEntry()方法也是首先判断是否有定制的Comparator,而后经过Comparator去遍历查询出须要的节点,遍历方式和put方法一致。
综上:TreeMap本质上就是一颗红黑树,而每一个Entry就是树的一个节点。