表1 Map集合常用方法
方法声明 | 功能描述 |
void put(Object key, Object value) | 向Map集合中添加指定键值映射的元素 |
int size() | 返回Map集合键值对映射的个数 |
Object get(Object key) | 返回指定键所映射的值,如果此映射不包含该键的映射关系,则返回null |
boolean containsKey(Object key) | 查看Map集合中是否存在指定的键对象key |
boolean containsValue(Object value) | 查看Map集合中是否存在指定的值对象value |
Object remove(Object key) | 删除并返回Map集合中指定键对象Key的键值映射元素 |
void clear() | 清空整个Map集合中的键值映射元素 |
Set keySet() | 以Set集合的形式返回Map集合中所有的键对象Key |
Collection values() | 以Collection集合的形式返回Map集合中所有的值对象Value |
Set<Map.Entry<Key,Value>> entrySet() | 将Map集合转换为存储元素类型为Map的Set集合 |
Object getOrDefault(Object key, Object defaultValue) | 返回Map集合指定键所映射的值,如果不存在则返回默认值defaultValue(JDK 8新方法) |
void forEach(BiConsumer action) | 通过传入一个函数式接口对Map集合元素进行遍历(JDK 8新方法) |
Object putIfAbsent(Object key, Object value) | 向Map集合中添加指定键值映射的元素,如果集合中已存在该键值映射元素,则不再添加而是返回已存在的值对象Value(JDK 8新方法) |
boolean remove(Object key, Object value) | 删除Map集合中键值映射同时匹配的元素(JDK 8新方法) |
boolean replace(Object key, Object value) | 将Map集合中指定键对象Key所映射的值修改为value(JDK 8新方法) |
集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。
图1 HashMap集合内部结构及存储原理图
水平方向以数组结构为主体并在竖直方向以链表结构进行结合的就是HashMap中的哈希表结构。在哈希表结构中,水平方向数组的长度称为HashMap集合的容量(capacity),竖直方向每个元素位置对应的链表结构称为一个桶(bucket),每个桶的位置在集合中都有对应的桶值,用于快速定位集合元素添加、查找时的位置。
当向HashMap集合添加元素时,首先会调用键对象k的hash(k)方法,快速定位并寻址到该元素在集合中要存储的位置。在定位到存储元素键对象k的哈希值所对应桶位置后,会出现两种情况:第一种情况,键对象k的hash值所在桶位置为空,则可以直接向该桶位置插入元素对象;第二种情况,键对象k的hash值所在桶位置不为空,则还需要继续通过键对象k的equals(k)方法比较新插入的元素键对象k和已存在的元素键对象k是否相同,如果键对象k相同,就会对原有元素的值对象v进行替换并返回原来的旧值,否则会在该桶的链表结构头部新增一个节点来插入新的元素对象。
HashMap的桶数目,就是集合中主体数组结构的长度,由于数组是内存中连续的存储单元,它占用的空间代价是很大的,但是它的随机存取速度是Java集合中最快的。通过增大桶的数量,而减少Entry<k,v>链表的长度,来提高从HashMap中读取数据的速度,这是典型的拿空间换时间的策略。
但是我们不能刚开始就给HashMap分配过多的桶,这是因为数组是连续的内存空间,它的创建代价很大,况且我们也不能确定给HashMap分配多大的空间才够合理,为了解决这一个问题,HashMap内部采用了根据实际情况,动态地分配桶数量的策略。
HashMap这种动态分配桶的数量,是通过new HashMap()方法创建HashMap时,会默认集合容量capacity大小为16,加载因子loadFactor为0.75(HashMap桶多少权衡策略的经验值),此时该集合桶的阀值就为12(容量capacity与加载因子loadFactor的乘积),如果向HashMap集合中不断添加完全不同的键值对<k,v>,当超过12个存储元素时,HashMap集合就会默认新增加一倍桶的数量(也就是集合的容量),此时集合容量就变为32。
如果开发者对存取效率要求的不是太高,想节省点空间的话,可以使用new HashMap(int initialCapacity, float loadFactor)构造方法,在创建HashMap集合时指定集合容量和加载因子,并将这个加载因子设置得大一些。
Map集合遍历
第一种方式,可以使用Iterator迭代器遍历集合;第二种方式就是使用JDK 8提供的forEach(Consumer action)方法遍历集合。接下来,就以前面学习的HashMap集合为例来分别对这两种集合遍历方式进行详细讲解。
1.Iterator迭代器遍历Map集合
需要先将Map集合转换为Iterator接口对象,然后进行遍历。keySet()方法和entrySet()方法。
keySet()方法,需要先将Map集合中所有键对象转换为Set单列集合,接着将包含键对象的Set集合转换为Iterator接口对象,然后遍历Map集合中所有的键,再根据键获取相应的值。
import java.util.*;
public class Example15 {
public static void main(String[] args) {
Map map = new HashMap(); // 创建Map集合
map.put("1", "Jack"); // 存储元素
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
Set keySet = map.keySet(); // 获取键的集合
Iterator it = keySet.iterator(); // 迭代键的集合
while (it.hasNext()) {
Object key = it.next();
Object value = map.get(key); // 获取每个键所对应的值
System.out.println(key + ":" + value);
}
}
}
entrySet()方法,该方法将原有Map集合中的键值对作为一个整体返回为Set集合,接着将包含键值对对象的Set集合转换为Iterator接口对象,然后获取集合中的所有的键值对映射关系,再从映射关系中取出键和值。
import java.util.*;
public class Example15 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
Set entrySet = map.entrySet();
Iterator it = entrySet.iterator(); // 获取Iterator对象
while (it.hasNext()) {
// 获取集合中键值对映射关系
Map.Entry entry = (Map.Entry) (it.next());
Object key = entry.getKey(); // 获取Entry中的键
Object value = entry.getValue(); // 获取Entry中的值
System.out.println(key + ":" + value);
}
}
}
2.JDK 8新方法遍历集合Map集合
与Collection集合遍历类似,在JDK 8中也根据Lambda表达式特性新增了一个forEach(BiConsumer action)方法来遍历Map集合,该方法所需要的参数也是一个函数式接口,因此可以使用Lambda表达式的书写形式来进行集合遍历。
import java.util.HashMap;
import java.util.Map;
public class Example16 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
// 使用JDK 8新增的forEach(BiConsumer action)方法遍历集合
map.forEach((key,value) -> System.out.println(key + ":" + value));
}
}
多学一招:使用LinkedHashMap集合保证元素添加顺序。如果想让这两个顺序一致,可以使用Java中提供的LinkedHashMap类,它是HashMap的子类,和LinkedList一样也使用双向链表来维护内部元素的关系,使LinkedHashMap元素迭代的顺序与存入的顺序一致
import java.util.*;
public class Example18 {
public static void main(String[] args) {
Map map1 = new HashMap(); // 创建HashMap集合
map1.put(2, "Rose");
map1.put(1, "Jack");
map1.put(3, "Lucy");
map1.forEach((key,value) -> System.out.println(key + ":" + value));
System.out.println("=====================");
Map map2 = new LinkedHashMap(); // 创建LinkedHashMap集合
map2.put(2, "Rose");
map2.put(1, "Jack");
map2.put(3, "Lucy");
map2.forEach((key,value) -> System.out.println(key + ":" + value));
}
}
一般情况下,我们用的最多的是HashMap,在Map中插入、删除和定位元素,HashMap 是最好的选择。但如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列。
TreeMap集合
取出的元素按照键对象的自然顺序进行了排序,这是因为添加的元素中键对象是String类型,String类实现了Comparable接口,因此默认会按照自然顺序对元素进行排序。
文件2 Example20.java
import java.util.*;
// 自定义比较器
class CustomComparator implements Comparator {
public int compare(Object obj1, Object obj2) {
String key1 = (String) obj1;
String key2 = (String) obj2;
return key2.compareTo(key1); // 将比较之后的值返回
}
}
public class Example20 {
public static void main(String[] args) {
Map map = new TreeMap(new CustomComparator());
map.put("2", "Rose");
map.put("1", "Jack");
map.put("3", "Lucy");
System.out.println(map);
}
}
运行结果如图2所示。
Properties集合
Map接口还有一个实现类Hashtable,它和HashMap十分相似,其中一个主要区别在于Hashtable是线程安全的。另外在使用方面,Hashtable的效率也不及HashMap,所以,目前基本上被HashMap类所取代,但Hashtable类有一个子类Properties在实际应用中非常重要。
Properties主要用来存储字符串类型的键和值,在实际开发中,经常使用Properties集合类来存取应用的配置项。
泛型
在程序中无法确定一个集合中的元素到底是什么类型,那么在取出元素时,如果进行强制类型转换就很容易出错。
泛型可以限定操作的数据类型,在定义集合类时,可以使用“<参数化类型>”的方式指定该集合中存储的数据类型,具体格式如下:
ArrayList<参数化类型> list = new ArrayList<参数化类型>();
需要注意的是,在使用泛型后每次遍历集合元素时,可以指定元素类型为String,而不是Object,这样就避免了在程序中进行强制类型转换。