Map
★Map接口和常用方法
public interface Map<K,V>
Map
接口实现类的特点:Map
与Collection
并列存在。用于保存具有映射关系的数据:**Key-Value
**(双列元素)。Map
中的key
和value
可以是任何引用类型的数据,会封装到HashMap$Node
对象中。Map
中的key
不允许重复,原因和HashSet
一样,前面分析过源码。**注意:当
key
重复时,后面put
的value
会替换前面put
的value
**,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.f.chapter14.map;
import java.util.HashMap;
import java.util.Map;
/**
* @author fzy
* @date 2023/6/21 14:50
*/
public class Map01 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("num1", "java");
map.put("num2", "go");
map.put("num1", "python");
System.out.println(map);
}
}输出结果为:
1
{num1=python, num2=go}
Map
中的value
可以重复。Map
的key
可以为null
,value
也可以为null
。注意
key
可以为null
,但是只能有一个;value
可以为null
,但是可以多个。经常使用
String
类作为Map
的key
。key
和value
之间存在单向一对一关系,即通过指定的key
总能找到对应的value
。如下图是
Map
存放数据的key-value
示意图,一对key-value
最终是放在一个HashMap$Node
中的,又因为Node
实现了Entry
接口:static class Node<K,V> implements Map.Entry<K,V>
,所以有些书上也说“一对k-v
就是一个Entry
”。
另外,
key-value
为了方便程序员的遍历,还会创建entrySet
集合 :transient Set<Map.Entry<K,V>> entrySet;
,该集合存放的元素的类型为HashMap$Node
,该类型实现了Entry
接口,而一个Entry
对象就有key
(指向HashMap$Node
中的key
)、value
(指向HashMap$Node
中的value
)。当把
HashMap$Node
对象存放到entrySet
后,就方便了我们的遍历,因为Map.Entry
提供了重要的方法:getKey
和getValue
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28package com.f.chapter14.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author fzy
* @date 2023/6/21 14:50
*/
public class Map01 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("num1", "java");
map.put("num2", "go");
map.put("num1", "python");
map.put("num3", "php");
map.put(1, "hello");
map.put(2, "world");
Set set = map.entrySet();
System.out.println(set.getClass()); //class java.util.HashMap$EntrySet
for (Object obj : set) {
//System.out.println(obj.getClass()); //class java.util.HashMap$Node
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}最后回到这张图,一对
k-v
存储到EntrySet
中的流程如下:
如果我们只想得到
key
的集合,也可以只使用Set set = map.keySet();
如果我们只想得到
value
的集合,也可以只使用Collection values = map.values();
Map
接口的常用方法:put(K key, V value)
:向map
中添加一对k-v
,如果键key
重复,则会对value
进行覆盖替换。 -> 增 + 改remove(Object key)
:根据键删除映射关系。 -> 删get(Object key)
:根据键获取值。 -> 查size()
:获取k-v
对的个数。isEmpty()
:判断map
是否为空。clear()
:清空map
。containsKey(Object key)
:查找键是否存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package com.f.chapter14.map;
import java.util.HashMap;
import java.util.Map;
/**
* @author fzy
* @date 2023/6/22 9:39
*/
public class MapMethod01 {
public static void main(String[] args) {
Map map = new HashMap();
//1.`put(K key, V value)`:向 `map` 中添加一对 `k-v` ,如果键 `key` 重复,则会对 `value` 进行覆盖替换。
map.put("num1", "三国演义");
map.put("num2", "石头记");
map.put("num2", "红楼梦"); //“红楼梦”替换了“石头记”
map.put("num3", "水浒传");
map.put("num4", "西游记");
map.put(null, "四大名著");
System.out.println(map); //{null=四大名著, num1=三国演义, num4=西游记, num3=水浒传, num2=红楼梦} -> 注意是无序的
//2.`remove(Object key)`:根据键删除映射关系。
map.remove(null);
System.out.println(map); //{num1=三国演义, num4=西游记, num3=水浒传, num2=红楼梦}
//3.`get(Object key)`:根据键获取值。
System.out.println(map.get("num2")); //红楼梦
//4.`size()`:获取 `k-v` 对的个数。
System.out.println(map.size()); //4
//5.`isEmpty()`:判断 `map` 是否为空。
System.out.println(map.isEmpty()); //false
//6.`clear()`:清空 `map`。
map.clear();
System.out.println(map); //{}
//7.`containsKey(Object key)`:查找键是否存在。
System.out.println(map.containsKey(null)); //false
}
}
Map六大遍历方式
- 分别是
KeySet
、values
和entrySet
的增强 for
和iterator
遍历。
1 | package com.f.chapter14.map; |
★★★Map.Entry相关说明
对
Map.Entry
相关再做一个说明:首先要明确,
Entry
是一个接口,而且是Map
接口的内部接口。而HashMap
的静态内部类Node
是实现了Map.Entry
接口。**真正存放
k-v
的地方是HashMap$Node
**,即HashMap
的静态内部类Node
。EntrySet
是用于方便程序员对map
中的元素进行遍历,因为Map.Entry
有两个抽象方法:getKey
和getVal
,通过这两个方法我们可以获取map
中的k-v
的具体的内容。因为
HashMap$Node
实现了Map.Entry
接口,所以其实现了getKey
和getVal
方法,按理来说,在进行遍历时(以第五种方式为例),我们可以直接用HashMap.Node node = (HashMap.Node) obj;
,然后再使用node.getKey()
和node.getVal()
来获取k-v
值,但实际是不行的。原因在于Node
是HashMap
的静态内部类,并没有被public
关键字修饰,所以无法在包外被使用。所以采用
Map.Entry
的方式 (Map.Entry entry = (Map.Entry) obj;
)来调用entry.getKey()
和entry.getVal()
方法,这里是用到了接口的多态(obj
实际的运行类型是HashMap$Node
)。
★★★HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap
是Map
接口使用频率最高的实现类。HashMap
以key-value
对的方式来存储数据(HahsMap$Node
类型)。key
不能重复,但是值可以重复,允许使用null
键和null
值。- 如果添加相同的
key
,则会覆盖原来的key-val
,等同于修改。(key
不会替换,val
会替换) - 与
HashSet
一样,不保证存入和取出的顺序一样,因为底层是以hash
表的方式来存储的。 HashMap
没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
。
HashMap底层机制
因为
HashSet
底层就是HashMap
,所以前面介绍的HashSet
底层机制本质就是HashMap
的底层机制,即HashMap
底层是(数组+链表+红黑树)。
扩容机制:和
HashSet
的扩容机制是完全一样的,还是再简单总结一遍:HashMap
底层维护了Node
类型的数组HashMap$Node[] table
,默认为null
。- 当创建
HashMap
对象时,先将加载因子 (loadfactor
) 初始化为 0.75,且table
初始为 null。 - 当添加
key-val
时,通过key
的哈希值得到在tablel
的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key
和准备加入的key
是否相等,如果相等,则直接替换val
;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。 - 第 1 次添加,则需要扩容
table
容量为16,临界值 (threshold
) 为12。 - 以后再扩容,则需要扩容
table
容量为原来的 2 倍,即 32,临界值为原来的 2 倍,即24,依次类推。 - 在 Java8 中,如果一条链表的元素个数超过
TREEIFY THRESHOLD
(默认是8),并且table
的大小>=MIN TREEIFY CAPACITY
(默认64),就会进行树化(红黑树)。
Hashtable
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
- 存放的元素是键值对:即
K-V
。 Hashtable
的键和值都不能为null
,否则会抛出NullPointerException
异常。Hashtable
使用方法基本上和HashMap
一样。Hashtable
是线程安全的(synchronized
),HashMap
是线程不安全的。
Hashtable扩容机制
Hashtable
底层有数组Hashtable$Entry[]
,初始化大小为 11,加载因子为 0.75,初始threshold
为 8(11*0.75
,丢弃小数部分)。1
2
3public Hashtable() {
this(11, 0.75f);
}1
2
3
4
5
6
7
8
9
10
11
12
13public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}在添加
k-v
元素时,put
函数会调用addEntry(hash, key, value, index);
函数来添加。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public synchronized V put(K key, V value) {
// Make sure the value is not null
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;
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;
}
}
addEntry(hash, key, value, index);
return null;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}在
addEntry
函数中可以看到,如果(count >= threshold)
,就会rehash
,即进行扩容。扩容机制为int newCapacity = (oldCapacity << 1) + 1;
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1; //新的容量=(旧的容量*2+1)
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;
}
}
}
Properties
Properties
类继承自Hashtable
类并且实现了Map
接口,也是使用一种键值对的形
式来保存数据。- 它的使用特点和
Hashtable
类似。 Properties
还可以用于从xxx.properties
文件中,加载数据到Properties
类对象,
并进行读取和修改。- 说明:工作后
xxx.properties
文件通常作为配置文件,这个知识点会在IO
流举例。
TreeMap
TreeMap
和TreeSet
大同小异,因为TreeSet
底层就是TreeMap
。- 只不过在
TreeSet
中,k-v
中的v
都是同一个对象(作为占位作用):private static final Object PRESENT = new Object();
。 - 在
TreeMap
中,k-v
中的v
可以有不同的值。
- 只不过在
★注意:在
HashMap
中,判断key
是否相同是通过源码中的1
2if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))来判断的。
而在
TreeMap
中,判断key
是否相同是通过我们传入的Comparator
对象来判断的。1
2
3
4
5
6TreeMap treeMap = new TreeMap(new Comparator() {
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String)o2);
}
});
★★★集合选型规则
开发中如何选择集合实现类:
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
- 先判断存储的类型(一组对象或一组键值对
k-v
)。 - 如果是一组对象:
Collection
接口:- 允许重复:
List
- 增删多:
LinkedList
(底层维护了一个双向链表) - 改查多:
ArrayList
(底层维护了Object
类型的可变数组)
- 增删多:
- 不允许重复:
Set
- 无序:
HashSet
(底层是HashMap
,维护了一个哈希表,即(**数组+链表+红黑树
**)。 - 排序:
TreeSet
- 插入和取出顺序一致:
LinkedHashSet
(维护数组+双向链表)
- 无序:
- 允许重复:
- 如果是一组键值对:
Map
接口:- 键无序:
HashMap
(底层是:哈希表。jdk7:数组+链表,**jdk8:数组+链表+红黑树
**) - 键排序:
TreeMap
- 键插入和取出顺序一致:
LinkedHashMap
- 读取文件:
Properties
- 键无序:
- 先判断存储的类型(一组对象或一组键值对
Collections工具类
Collections
是一个操作Set
、List
和Map
等集合的工具类。Collections
中提供了一系列静态的方法,用以对集合元素进行排序、查询和修改等操作。
排序操作
reverse(List<?> list)
:反转List
中的元素的顺序。shuffle(List<?> list)
:对List
集合元素进行随机排序。sort(List<T> list)
:根据元素的自然顺序对指定List
集合元素按升序排序。sort(List<T> list, Comparator<? super T> c)
:通过传入Comparator
来指定排序的方式,对List
集合元素进行排序。swap(List<?> list, int i, int j)
:将List
中索引为i
和j
的集合元素对调位置。
1 | package com.f.chapter14.collections; |
查找操作
max(Collection<? extends T> coll)
:根据元素的自然顺序,返回给定集合中的最大元素。max(Collection<? extends T> coll, Comparator<? super T> comp)
:根据传入的Comparator
指定的顺序,返回给定集合中的最大元素。min(Collection<? extends T> coll)
:和1.
类似。min(Collection<? extends T> coll, Comparator<? super T> comp)
:和2.
类似。frequency(Collection<?> c, Object o)
:返回指定集合中指定元素的出现次数。
1 | package com.f.chapter14.collections; |
修改操作
copy(List<? super T> dest, List<? extends T> src)
:将src
中的内容复制到dest
中。replaceAll(List<T> list, T oldVal, T newVal)
:使用newVal
替换List
对象的所有oldVal
。
1 | package com.f.chapter14.collections; |