集合、Map-2、Set、HashMap、HashTable
Set
set接口基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
- 底层是HashMap<>();
接口和常用方法
- 和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
- 遍历:可以用迭代器、for each、但是不能用索引的方式来获取,而且set接口也没有get(int index)方法;
HashSet源码分析:
-
HashSet 构造器:除了一个特殊的,其他底层都先创建了一个HashMap。
public HashSet() { map = new HashMap<>(); } public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } /* Params: initialCapacity – the initial capacity of the hash map loadFactor – the load factor of the hash map dummy – ignored (distinguishes this constructor from other int, float constructor.) =================将次构造器与其他int,float构造器区别开、这个主要是用于支持LinkedHashSet======= */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
-
add( ):添加元素
整体思路:
-
添加元素时,将element看作HashMap中的key,并用Object空对象PRESENT作为HashMap中的value占位符。先得到element的hashCode值,然后将此hashCode值转为HashMap的索引值hash,
-
找到HashMap中的table,看对应的hash索引位置是否有元素
-
如果没有,直接把新建一个Node,把<hash(element.hashCode()), element(K), PRESENT(value) >存储在此处
-
如果该处存储的有某个对象ele1,那么继续后面的判断
-
首先需要比较hash是否相等,不相等,则直接判断下面的情况2,若相同,还需比较element对象地址和ele1对象地址是否相同,或者用ele1.equal(element)比较是否相等,若其中一个相同,则此次添加将无效,并返回element
//这个条件满足的话,会插入失败。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
-
若1没有满足,则检查这里存放ele1的节点是否是一个树节点,如果是的话,将element按照树的添加方法添加进去
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
-
若2也没有满足,则说明存放ele1的节点是个链中的一个节点,那么就循环的往后比较看ele1后面的各元素是否和要添加的元素element相等,如果相同,则放弃添加,如果都不同,则添加到最后。此处添加完,还需查记录当前节点是插入该链的第几个节点,若是该链插入此节点后,超过了8个节点 ( 注意是超过了8个,也就是说当前节点插进去之后,这个链变成了9个节点,才会考虑将该链转为红黑树 ) ,则判断是否需要将该链转换为红黑树:若HashMap.table为空或者长度小于64(HashMap.MIN_TREEIFY_CAPACITY)那么,只对HashMap.table进行resize()扩容即可,否则将该链变为红黑树。
(这里用8作为分界线是需要综合考虑空间和效率的均衡问题,当用红黑树时,每个节点占用的空间交大,但用单链时候,查询效率时线性n,但小于8的时候,也会很快。在理想条件下,一个链所能有的长度符合泊松分布,也就是某个链长度达到8时,其概率只有0.00000006,而若这种情况下,某个链长度还大于8了,那说名很大问题就是程序不够理想,把很多节点都放到一个链上了,此时为了查询效率不会下降,就舍弃空间问题,转为红黑树,源码中有一下注释。)。* usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
-
最后若添加成功了,则++size,++modCount,其中 ++modCount 表示该HashMap被修改过的次数加一,并根据新size判断是否需要resize()扩容:如:当超过临界值(threshold)12时,也就是添加第13个的元素的时候,将会扩容。
.... .... ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
-
-
resize()扩容:
-
HashSet底层是HashMap,第一次添加的时候,table数组扩容到16,并将临界值(threshold)设置为12,其中装载因子设为0.75是因为即需要尽量少发生hash碰撞,又需要尽量不浪费空间,权衡而来的:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //定义默认初始容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; //定义装载因子 newCap = DEFAULT_INITIAL_CAPACITY; //第一次扩容时扩容为16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //第一次扩容时将临界值转为16 * 0.75 = 12
-
如果table数组使用到了临界值12,也就是放置完第13个元素的时候,就是会扩容到16 * 2 = 32,新的临界值依然是新容量的0.75为:32 * 0.75 = 24
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold
-
在JAVA8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认8),且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则继续采用数组扩容机制。
//add函数进来,首先调了HashMap的put()方法; public boolean add(E e) { //返回为空,则说明添加成功了,要不然返回一个需要添加的值e,说明未添加成功 return map.put(e, PRESENT)==null; } //HastMap的put方法又调用了自己的putVal()方法: //其中传过来的key值是上层HashSet想要添加的element对象,值是HashSet的成员属性-->PRESENT //但这个present实际没什么意义,只是为了让HashSet能用上HashMap而进行的一个占位符,占位HashMap<K,V>中的 V //其定义如下: /* Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //除此之外,我们还注意到hash(key),算得一个int类型得hash值, //其具体算法如下: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //第一次进来得时候,因为tab = table是空的,所以先吧tab = resize了一下,而resize()做了什么呢,看下面 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //第一次往里面添加元素时,table为空,走这个地方 //table是HashMap的成员属性, if ((tab = table) == null || (n = tab.length) == 0) //table第一次putVal得时候,是空的,所以在这里resize之后, //tab得长度经过默认初始化长度,成了16,且其阈值threshold = 0.75 * length = 12 //0.75是设定 是权衡了 防止hash容易冲突 和 空间利用 而定下来的值 //也就是第一次扩容的时候,扩容到空间为16,而扩容也只能是以2的倍数扩容,因为在下一行 // p = tab[i = (n-1) & hash]时,这里其实是一个取模的位运算,但是需要保证n必须是2^n, // 相当于为了取模计算的快点,使得空间最开始就需要以2的倍数去扩容 n = (tab = resize()).length; // 令p指向 与想要放置节点 hash位置 相同的那个地方,如果p是空的,表示该位置没有存放过元素, // 没存放过的话,在此新建一个节点,然后就跳到最后++modCount,然后返回就可以了 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果在往table上hash的时候,冲突了,就分情况讨论判断了 else { Node<K,V> e; K k; //条件1:走到这里的话,说名p指向的这个地方,不是空的,而是放过节点的,那么首先比较p这个节点, //与当前想要放置的节点,他们的key是否是同一个对象,或者他们的内容相同(equal返回true) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //条件2:如果上面条件1的判等条件没进去,那么接下来判断p是不是一个红黑树节点,是的话,就将他插入红黑树 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //条件3:如果上面的条件2也不是,那么意味着,这个节点跟p节点不同,而且p节点处也是一个链表 //那么就把这个需要放置的节点挂到链表上 else { for (int binCount = 0; ; ++binCount) { //如果p的下个节点e为空,直接放,放完判断容量到没到需要变成树(判断加入新节点后,整个 //链的长度到是否到达了TREEIFY_THRESHOLD,即到达了8(但其实仔细看的话,这时候判断大于8是指的当插 //入完新节点之后,该链表总共有9个元素的时候,开始尝试变成树),TREEIFY_THRESHOLD是一个final定义的int 8), //而为什么这里要设置为8呢,因为当数量小于8的时候,链表查询效率高,而大于8的时候,红黑树效率高, //最后判断完,就break,跳出for //需要注意的是,进入treeifyBin之后,不一定真的会变成树,因为在该函数内,还需要判断tab的长度是否达到了 //64(MIN_TREEIFY_CAPACITY),没到的话首先会扩容,否则才会树化 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //p的下个节点e不空,那么比较e的key和当前的key是不是一样,一样的话break,不一样的话继续往后找, //直到走到链表的尾部 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //放完node之后,再判断需不需要扩容 if (++size > threshold) resize(); //这是一个空的函数,而起作用是为了以后其他类继承HashMap的时候,可以在最后做一些东西,比如使链表成为有序链表之类的 afterNodeInsertion(evict); //返回true,代表成果插入了 return null; } // resize函数: final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
-
LinkedHashSet:
-
基本说明:
-
LinkedHashSet时HashSet的子类。
-
LinkedHashSet底层是一个 LinkedHashMap,LinkedHashMap底层又维护了一个数组+双向链表。
//HashSet里面: HashSet(int initialCapacity, float loadFactor, boolean dummy) { //因为1.HashSet拥有私有属性map(<E,Object>),而2.LinkedHashSet继承的HashSet,私有属性map需要是LinedkHashMap //所以在构造1的时候,map初始华为一个HashMap、而子类LinkedHashSet调用父类HashSet,初始化map为LinedHashMap的时候, //需要区分map到new出来什么对象,而dummy就是用来区分这个的。具体关系如下 // // HashSet: 属性map的类型 ------------> HashMap // ^ ^ // |(继承) |(继承) // | | // LinkedHashSet: 属性map的类型 ------> LinkedHashMap) map = new LinkedHashMap<>(initialCapacity, loadFactor); } //LinkedHashSet public LinkedHashSet() { super(16, .75f, true); }
-
LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
-
LinkedHashSet不允许添重复元素。
-
-
底层结构
-
add():添加元素(其实LinkedHashSet并没有显示的定义)
-
在LinkedHastSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail )
-
每一个节点有pre和next属性,这样可以形成双向链表
-
在添加一个元素时,先求hash值,在求索引.,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
tail.next = newElement!/简单指定
newElement.pre = tail
tail = newEelment; -
这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致
-
第一次添加元素的时候,LinkedHashMap的table数组将扩容到16。
-
(注*:LinkedHashMap中数组是HashMap N o d e [ ] , 但 存 放 的 节 点 类 型 是 L i n k e d H a s h M a p Node[],但存放的节点类型是LinkedHashMap Node[],但存放的节点类型是LinkedHashMapEntry,说明LinkedHashMap E n t r y 与 H a s h M a p Entry与HashMap Entry与HashMapNode存在某种继承或者实现关系,是一种多态)
//LinkedHashMap中有以下定义,可以看出来Entry是继承了HashMap的静态内部类:HashMap.Node<K,V> static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
-
Map
Map接口和常用方法:
Map接口实现类的特点[很实用],JDK8
-
Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
-
Map 中的key和 value可以是任何引用类型的数据,会封装到HashMap$Node
对象中 -
Map中的key 不允许重复,原因和HashSet 一样,前面分析过源码.
-
Map 中的value可以重复
-
Map的key可以为null, value也可以为null,注意key为null, 只能有一个,value为null,可以多个.
-
常用String类作为Map的key
-
key 和 value之间存在单向一对一关系,即通过指定的key 总能找到对应的value
-
测试:
import java.util.HashMap; public class Test06_HashMap { public static void main(String[] args) { HashMap hashMap = new HashMap(); hashMap.put("ljh",7000_0000); System.out.println("hashMap = " + hashMap); hashMap.put("ljh",777_7777); System.out.println("hashMap = " + hashMap); hashMap.put("LJH",777_7777); System.out.println("hashMap = " + hashMap); hashMap.put(null,null); hashMap.put(null,1); hashMap.put("mo",null); System.out.println("hashMap = " + hashMap); System.out.println("hashMap.get(\"LJH\") = " + hashMap.get("LJH")); } } //输出 //---------------------------------------------------------------------------- hashMap = {ljh=70000000} hashMap = {ljh=7777777} hashMap = {ljh=7777777, LJH=7777777} hashMap = {null=1, mo=null, ljh=7777777, LJH=7777777} hashMap.get("LJH") = 7777777 Process finished with exit code 0
HashMap(具体很多细节上面HashSet的时候已经提到)
- key不能重复,值可以重复,允许使用null键和null值。如果添加相同的key,则会覆盖原来的value(key不会替换,val会替换)
- k-v 最终存储的形式是 HashMap$ node = newNode(hash, key, value, null)。
- HashMap没有实现线程同步,因此是线程不安全的。
- k-v 为了方便程序员遍历,还会创建EntrySet集合,该集合存放的元素类型为Entry,而每一个Entry对象都有k,v,也可以有相应的getKey(),和getValue方法。 EntrySet<Entry<K, V>> 即 : transient Set<Map.Entry<K,V>> entrySet。EntrySet中定义的类型是Map.Entry,但实际还是指向的HashMap$Node,而且在HashMap中有如下定义:
static class Node<K,V> implements Map.Entry<K,V>
- EntrySet其实可以理解成视图,在HashMap中,整一个entrySet方法和EntrySet类,主要是为了方便遍历,因为entrySet中存放的Entry(是一个接口),有getKey(),getValue()方法,方便拿到值,并且entrySet中存放的HashMap.Node类实现了Map.Entry类,并实现了getKey(),getValue()等方法。
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的,(JDK8 HashMap底层是数组+链表(红黑树))
- HashMap没有实现同步,因此是线程不安全的,方法没有做互斥(Synchronized)
底层机制及源码分析:
-
- (K,V)是一个Node,实现了Map.Entry<K, V>,查看HashMap的源码可以看到。
- JDK7.0的hasmap底层实现[数组+链表],jdk8.0底层[数组 + 链表 + 红黑树]
- 扩容机制:
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75.
- 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要打容。
- 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(loadFactor=0.75,16*loadFactor=12).
- 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推.
- 在Java8中,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8),并且table的大小> = MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)。
HashTable
基本介绍:
- 存放的元素是键值对: 即K-V
- hashtable的键和值都不能为null,否则会抛出NullPointerException
- Hashtable使用方法基本上和HashMap一样
- Hashtable是线程安全的(synchronized),hashMap 是线程不安全的
简单底层说明:
- 底层有数组,Hashtable$Entry[] 初始化大小为11
- 临界值 threashold 8 = 11*0.75
- 扩容:按照自己的扩容机制进行扩容 [ newCapacity = (oldCapacity << 1) + 1]
HashMap-Hashtable对比
版本 | 线程安全(同步) | 效率 | 允许null键null值? | resize | 继承 | |
---|---|---|---|---|---|---|
HashMap | 1.2之后出来的 | 不安全 | 高 | 可以 | 无参构造,初始默认0,第一次扩容将table扩容为16,后续每次扩容为原来2倍(原因与位运算快速取余有关),loadFactor为容量的0.75(而且到一个链上挂8个节点之后,在挂第9个节点时,当前这一条链会考虑转为红黑树(但也需要满足此时table容量大于等于64)) | HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> |
Hashtable | 1.0之后出来的 | 安全 | 低 | 不可以 | 无参默认table容量为11,每次扩容为原来容量的2倍+1,loadFactor为容量的0.75 | Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V> |
Properties
- Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形
式来保存数据。 - 他的使用特点和Hashtable类似
- Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
- 说明:工作后xxx.properties 文件通常作为配置文件,这个知识点在IO流举例。
常用方法:
-
Collections.reverse(List):发转List中元素的顺序
-
Collections.shuffle(List):随机打乱
-
Collections.sort(List):自然排序
-
Collections.sort(List, new Comparator(){}):按comparator排序
-
Collections.swap(List, int i, int j):交换下表i,j的值
-
Collections.reverse(List):翻转表
-
Object Collections.max(Collection):自然排序情况下的最大值
-
Object Collections.max(Collection, new Comparator(){}):按comparator排序情况下的最大值
-
Object Collections.min(Collection):自然排序情况下的最小值
-
Object Collections.min(Collection, new Comparator(){}):按comparator排序情况下的最小值
-
int Collections.frequency(Collection, Object):返回指定集合中指定元素的出现次数
-
void Collections.copy(List dest, List src):将src中的内容复制到dest中
-
Boolean Collections.replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对向的所有旧值