HashMap底层机制及源码剖析
-
HashMap底层维护了Node类型的数组table,默认为null
-
当创建对象时,将加载因子初始化为0.75;
-
当添加key-value时,通过key的哈希值得到在table的索引,然后判断该索引处是否有元素,如果没有元素直接添加,如果该索引处有
元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理,如果添加时发现容量不够,则需要扩容 resize();
-
第一次添加,则需要扩容table容量为16,临界值threshold为12
-
以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推;
-
在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树);
HashMap源码解读
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果table的索引位置的key的hash和新的key的hash值相同,并
//满足(table 现有的结点的key和准备添加的key是同一个对象 || equals返回真,就认为不能加入新的k-v)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果当前的table的已有的Node是红黑树,就按照红黑树的方式来处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的结点,后面是链表,就循环比较
for (int binCount = 0; ; ++binCount) {//死循环
if ((e = p.next) == null) {//走到最后了,还没找到与之相同的
//加到链表的屁股后面
p.next = newNode(hash, key, value, null);
//判断是不是链表中加满了八个
if (binCount >= TREEIFY_THRESHOLD - 1) //>= (8-1=7)
//树化,当然这里不是立即树化哈,深挖进去,会发现还要满足table的长度>=64
//才会树化,反之不符合条件就选择扩容,不急着树化
treeifyBin(tab, hash);
//我找了一圈,发现没有找到和我一样的,就把我加到屁股后面,然后退出
break;
}
//循环比较的过程中,我发现有一个是相同的,即hash值也相同||值相同,我就退出
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;//替换找到的重复key对应的value
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每成功增加一个,就++
if (++size > threshold)//12--24--48,一旦发现到了threshold临界值,就进入分支开始扩容
resize();
afterNodeInsertion(evict);
return null;
}
//树化部分的源码
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果table暂时为null,或者大小还没有到 64,暂时不树化,而是进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if (
.......
}
}
HashTable
- 存放的元素是键值对: 即 k-v
- hashtable的键和值都不能为null,否则会抛出空指针异常
- hashTable使用方法基本上和HashMap一样
- hashTable是线程安全的(因为有synchronized修饰),hashMap是线程不安全的
- 底层源码剖析
//简单说明一下HashTable的底层
/**
1.底层有数组Hashtable$Entry[] 初始化大小为11
2.临界值 threshold 为 8--> 11*0.75;
3.扩容:按照自己的扩容机制来进行
4.By 执行 addEntry(hash, key, value, index);
添加K-V ,将其封装到Entry中去
5.当if (count >= threshold) 满足时,就进行扩容
6.按照int newCapacity = (oldCapacity << 1) + 1; 的大小扩容
*/
public synchronized V put(K key, V value) {
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//hashtable的底层是entry数组,通过以下的add方法将key、value放进entry中,然后添加到数组中去
addEntry(hash, key, value, index);
return null;
}
}
//addEntry方法底层
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//当存放的元素数量大于临界值
if (count >= threshold) {
//进入这个方法
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
//rehash()底层源码
@SuppressWarnings("unchecked")
protected void rehash() {
//oldCapacity起初为11嘛
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 扩容后新的容量=oldCapacity乘以2,再+1;
//这就是起初容量为11,之后扩容后变为11*2+1=23的原因
int newCapacity = (oldCapacity << 1) + 1;
//如果新容量-数组最大值2147483639仍然大于0,则进入分支
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
HashMap和HashTable之间的对比
线程安全(同步) | 效率 | 允许null键 null值 | |
---|---|---|---|
HashMap | 不安全 | 高 | 可以 |
HashTable | 安全 | 较低 | 不可以 |
Properities
- Properities类继承自HashTable类并且实现了Map接口,也是使用了一种键值对的形式来保存数据
- 它的使用特点和HashTable类似
- Properities还可以用于从xxx.properities文件中,加载数据到Properties类对象,并进行读取和修改
- 说明:工作后,xxxx.properities文件通常作为配置文件,这个知识点在IO流举例;
TreeSet
- 自动排序,TreeSet中的元素会自动按照自然顺序,例如数字从小到大,字母按照字典顺序,或者通过自定义的比较器排列;
- 不允许重复元素,TreeSet会检查插入的元素是否已经存在,如果存在相同的元素,新的元素将不会被添加
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
//1.当我们使用无参构造器,创建TreeSet时,仍然是无序的
treeSet.add("jason");
treeSet.add("R");
treeSet.add("K");
treeSet.add("AAA");
System.out.println(treeSet);
//[AAA, K, R, jason]
//2.我希望添加的元素,可以按照字符串大小来排序
//3.我们可以使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
// 并指定排序规则
TreeSet treeSetNB = new TreeSet(new Comparator() {
//源码解读,当传给TreeSet一个比较器时,深挖底层,我们会发现其将传入的比较器对象 赋给了TreeSet底层的
//TreeMap的属性this.comparator,也就是将匿名对象,传给了底层TreeMap的comparator属性
/**
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}*/
//在调用treeSet.add("AAA")时,在底层会执行到
/**
//此中的cpr就是我们的匿名内部类(对象)
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//动态绑定到我们的匿名内部类(对象) compare
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
//如果相等,即返回0,那么这个key就没有加入
else
return t.setValue(value);
} while (t != null);
}
*/
@Override
public int compare(Object o1, Object o2) {
//这里的这句话调用字符串的compareTo方法,默认按照字符串来比较大小
//这里实现从大到小的比较规则
return ((String)o2).compareTo((String)o1);
}
});
treeSetNB.add("jason");
treeSetNB.add("K");
treeSetNB.add("R");
treeSetNB.add("AAA");
System.out.println("添加了比较器后的treeSet:"+treeSetNB);
}
TreeMap
-
键的排序:TreeMap按照键的自然顺序,例如数字从小到大,字母按字典顺序,或者根据提供的Comparator 进行排序;
因此,插入元素时会自动排序,而不是按照插入的顺序存储;
-
不允许null键:TreeMap不允许null作为键,这与HashMap允许一个nulll键不同,但是TreeMap允许null值,可以有多个null值;
public static void main(String[] args) {
TreeMap treeMap = new TreeMap();
treeMap.put("1",null);
treeMap.put("2",null);
System.out.println(treeMap);
//{1=null, 2=null}
}
Collection工具类
- Collection是一个操作Set、List和Map等集合的工具类
- Collection中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
- 排序操作
- reverse(List):反转List中元素的顺序
- shuffle(List):对List集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定List集合元素按升序排序
- sort(List,Comparator):根据指定的Comparator 产生的顺序对List 集合元素进行排序
- swap(List,int,int):将指定List集合中的 i 处元素和 j 处元素进行交换
public static void main(String[] args) {
//创建ArrayList 集合,用于测试。
ArrayList list = new ArrayList();
list.add("kerwin");
list.add("h活在当下hhh");
list.add("AA感受每一段旅程");
list.add("moment");
System.out.println(list);
//[kerwin, h活在当下hhh, AA感受每一段旅程, moment]
//reverse 反转
Collections.reverse(list);
System.out.println(list);
//[moment, AA感受每一段旅程, h活在当下hhh, kerwin]
//shuffle 随机排序
Collections.shuffle(list);
System.out.println(list);
//[h活在当下hhh, AA感受每一段旅程, kerwin, moment]
//sort 根据元素的自然顺序对指定的List集合元素按升序排序
Collections.sort(list);
System.out.println(list);
//[AA感受每一段旅程, h活在当下hhh, kerwin, moment]
//sort(list , Comparator) 传入比较器进行排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
String str1 = (String)o1;
String str2 = (String)o2;
return str2.compareToIgnoreCase(str1);
}
});
System.out.println(list);
//[moment, kerwin, h活在当下hhh, AA感受每一段旅程]
//swap 将指定List集合中的i 处元素和 j 处元素进行交换;
Collections.swap(list,0,1);b ` 1N2QWA
System.out.println(list);
//[kerwin, moment, h活在当下hhh, AA感受每一段旅程]
}
- 查找、替换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection ,Comparator):根据Comparator指定的顺序返回给定集合中的最大元素
- Object min(Collection)
- Object max(Collection,Comparator)
- int frequency(Collection ,Object):返回指定集合中指定元素的出现次数
- void copy(List dest, List src):将src 中的内容复制到dest中
- boolean replaceAll(List list ,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
public static void main(String[] args) {
//创建ArrayList 集合,用于测试。
ArrayList list = new ArrayList();
list.add("kerwin");
list.add("hhhh");
list.add("AAjourney");
list.add("moment");
System.out.println(list);
//[kerwin, hhhh, AAjourney, moment]
//Object max(Collection); 返回给定集合中的最大元素
System.out.println("自然顺序下的最大元素");
System.out.println(Collections.max(list));
//moment
//自定义比较器,然后返回自定义规则下的最大元素
//比如,我要返回长度最大的元素
Object maxObject = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
String str1 = (String)o1;
String str2 = (String)o2;
return str1.length()-str2.length();
/**
* return ((String)o1).length()-((String)o2).length();
*/
}
});
System.out.println("自定义比较器下的最长元素");
System.out.println(maxObject);
//AAjourney
//min(Collection)
System.out.println("自然顺序下的最小元素:");
System.out.println(Collections.min(list));
//AAjourney
//同样,实现自定义比较器,然后返回长度最小的元素
Object minObject = Collections.min(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
/**
* 这里想补充一点容易犯错的点:你要像下面这样写的时候,务必要留意括号的位置,
* ((String)o1) 这个对应的才是String,才会有length()API可以调用
* 如果不留意这一点,就会频频报错hhh
*
*/
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("自定义比较器下的最短元素");
System.out.println(minObject);
//hhhh
// int frequency(Collection Object) 返回指定集合中指定元素出现的次数
list.add("hhhh");
System.out.println(list);
//[kerwin, hhhh, AAjourney, moment, hhhh]
int hhhhFre = Collections.frequency(list, "hhhh");
System.out.println(hhhhFre);
//2 即"hhhh"这个对象在集合中出现了2次
//void copy(List dest,List src)
ArrayList testList = new ArrayList();
testList.add("Bingo");
testList.add("Bingo");
//将testList集合中的元素,复制到list中去
Collections.copy(list,testList);
System.out.println(testList);
//[Bingo, Bingo]
System.out.println(list);
//[Bingo, Bingo, AAjourney, moment, hhhh]
//boolean replaceAll(List list, Object oldVal, Object newVal)
Collections.replaceAll(list,"hhhh","hhhhhDamn");
System.out.println(list);
//[Bingo, Bingo, AAjourney, moment, hhhhhDamn]
}
拓展:比较器专题练习
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
list.add("kiwi");
//1.按照字符串的长度来 进行从短到长排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
System.out.println("按照字符串长度,由短到长排序如下:");
System.out.println(list);
//[kiwi, apple, banana, cherry]
//2.按照字符串的最后一个字符从大到小排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
/**
* 这样也行!!!
* char lastChar1 = o1.charAt(o1.length() - 1);
char lastChar2 = o2.charAt(o2.length() - 1);
return Character.compare(lastChar2, lastChar1); // 从大到小排序
*/
return o2.charAt(o2.length()-1)-o1.charAt(o1.length()-1);
}
});
System.out.println("按照字符串的最后一个字符从大到小排序:");
for(String obj : list){
System.out.print(obj+"\t");
}
//cherry kiwi apple banana
//3.按字符串中的元音字母数量从多到少排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return countVowels(o2) - countVowels(o1); // 从多到少排序
}
private int countVowels(String str) {
int count = 0;
for (char c : str.toLowerCase().toCharArray()) {
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
count++;
}
}
return count;
}
});
System.out.println();
System.out.println("按字符串中的元音字母数量从多到少排序,如下:");
System.out.println(list);
//[banana, kiwi, apple, cherry]
//4.按字符串字母逆序排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.toCharArray()[0]-o1.toCharArray()[0];
}
});
System.out.println("按字符串字母逆序排序");
System.out.println(list);
//[kiwi, cherry, banana, apple]
//5.按字符串是否以元音字母开头排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
boolean startsWithVowel1 = startsWithVowel(o1);
boolean startsWithVowel2 = startsWithVowel(o2);
if (startsWithVowel1 && !startsWithVowel2) {
return -1;
} else if (!startsWithVowel1 && startsWithVowel2) {
return 1;
} else {
return o1.compareTo(o2); // 如果开头情况相同,则按字母顺序排序
}
}
private boolean startsWithVowel(String str) {
char firstChar = Character.toLowerCase(str.charAt(0));
return firstChar == 'a' || firstChar == 'e' || firstChar == 'i' || firstChar == 'o' || firstChar == 'u';
}
});
System.out.println("按字符串是否以元音字母开头排序");
System.out.println(list);
//[apple, banana, cherry, kiwi]
}
//输出结果如下:
按照字符串长度,由短到长排序如下:
[kiwi, apple, banana, cherry]
按照字符串的最后一个字符从大到小排序:
cherry kiwi apple banana
按字符串中的元音字母数量从多到少排序,如下:
[banana, kiwi, apple, cherry]
按字符串字母逆序排序
[kiwi, cherry, banana, apple]
按字符串是否以元音字母开头排序
[apple, banana, cherry, kiwi]
集合总结
开发中如何选择集合实现类
- 先判断存储的类型(一组对象或一组键值对)
- 一组对象(单列数据):Collection接口
- 允许重复:List
- 增删多:LinkedList[底层维护了一个双向链表]
- 改查多:ArrayList[底层维护了Object 类型的可变数组]
- 不允许重复:Set
- 无序:HashSet [底层是HashMap,维护了一个哈希表,即数组+链表+红黑树]
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet 维护数组+ 双向链表
- 允许重复:List
- 一组键值对(双列):Map
- 键无序:HashMap [ 底层是:哈希表 jdk7:数组+链表 jdk8:数组+链表+红黑树 ]
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties