容器(十一):容器双雄之Map
标签: Java编程思想
在此之前把Collection系列进行了详细的分析,除了Set接口,这是因为Set接口虽然是Collection系列的,但是其内部是通过Map来实现的。下面我们就来对Map进行详细的剖析。
什么是Map
作为容器双雄之一的Map,是与Collection接口同一等级的根接口,它表示一个映射表(也称为关联数组),是一个键值对(key-value)的映射。我们可以类比数学中“函数”的概念。
在一个Map中,任意一个key都有一个唯一确定的value与其对应,这种将对象映射到其他对象的能力是一种解决编程问题的杀手锏。我们可以将容器组合起来从而快速地生成强大的数据结构。例如,研究那些拥有多套房产的土豪们,只需要一个Map<Person,List<House>>
.
我们通过三个角度来分析Map:
Key:
键(key)是以Set的形式保存的,我们称Map中用来保存的key的集合为KeySet
。由此可知key是不允许重复,因此键存储的对象需要重写 equals() 和 hashCode() 方法(用于Set判断是否为重复元素)。Value:
值(value)是以Collection的形式保存,我们称Map中用来保存的value的集合为Values
。由Collection的特性可知:Value相互之间是可以重复的。Entry
Entry是Map声明的一个静态内部接口,此接口为泛型,定义为Entry
Map的实现类
SortedMap:实现Map接口的有序的键值对接口。映射方式是根据其键的自然顺序进行排序的。
NavigableMap:继承SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法的接口。
AbstractMap:实现了Map中的绝大部分函数接口。它减少了“Map的实现类”的重复编码。
Dictionary:任何可将键映射到相应值的类的抽象父类。目前被Map接口取代。
TreeMap:有序散列表,实现SortedMap 接口,底层通过红黑树实现。
HashMap:是Map接口最经典的实现方法,是基于“拉链法”实现的散列表。底层采用“数组+链表”实现。一般用于单线程。
WeakHashMap:基于“拉链法”实现的散列表。其中的键是弱键。
HashTable:基于“拉链法”实现的散列表。效率较低,一帮用于多线程。
各个实现类的不同点,参考http://blog.csdn.net/chenssy/article/details/37909815
Map源码分析
Map虽然是个接口,但是Java 8 开始接口中也可以实现具体的方法了(通过default关键字),因此代码有点长。
但是我们还是要:撸!源!码!
关键方法
public interface Map<K,V> {
int size(); 返回map中key-value映射的数量,也就是Entry的数量
boolean isEmpty(); //如果map中没有key-value映射返回true
//如果map不含key映射,返回false,当key的类型不符合,抛出ClassCastException,当key是null且该map不支持key的值是null时,抛出NullPointerException
boolean containsKey(Object key);
//如果map含有一个以上的key映射的参数value,返回true,异常抛出的情况和containKey一样
boolean containsValue(Object value);
V get(Object key); //根据key得到对应的value
V put(K key, V value); //往map放入一对key-value映射
V remove(Object key); //根据key删除对应映射
void putAll(Map<? extends K, ? extends V> m); //将参数中的Map复制一份
void clear(); //清空map中所有的映射
Set<K> keySet(); //返回map中所有key的集合
Collection<V> values(); //返回map中所有value的集合:values
Set<Map.Entry<K, V>> entrySet(); //返回key-value的集合
boolean equals(Object o); //还未实现
int hashCode(); //还未实现
Set<Map.Entry<K,V>> entrySet() //表示一个映射项(里面有Key和Value),而Set<Map.Entry<K,V>>表示一个映射项的Set。Map.Entry里有相应的getKey和getValue方法,让我们能够从一个项中取出Key和Value,通常用于遍历Map。
内部类
Entry对应的是一个具体的“键值对”,也就是一个条目
interface Entry<K,V> {
K getKey(); //返回该条键值对所对应的key
V getValue(); //返回该条键值对所对应的value
V setValue(V value); //设置用新value替换旧value,返回值是旧value
boolean equals(Object o); //如果两个entry的映射一样,返回true
int hashCode(); //计算entry的hash code
//返回一个比较器,比较的规则是key的自然大小
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
//这里用的是lambda表达式
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
//返回一个比较器,比较规则是value的自然大小
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
//返回一个比较器,比较规则用参数传入,比较的是key
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
//返回一个比较器,比较规则用参数传入,比较的是value
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
default方法
在Java 8中Map接口提供了一些新的便利的方法。有很多涉及了Java 8 中的一些新特性,我也不是很懂。所以只学习一些典型的方法:
因为以下的Map方法都是以默认值方法的方式实现的,所以现有的Map接口的实现可以直接拥有这些在默认值方法中定义的默认行为,而不需要新增一行代码。
Map.getOrDefault(Object, V)
Map的新方法getOrDefault(Object,V)允许调用者在代码语句中规定获得在map中符合提供的键的值,否则在没有找到提供的键的匹配项的时候返回一个“默认值”。
/**
* 该方法返回指定位置的value或者设定默认的value,若存在key或所对应
* value不为null,则返回value,否则返回默认的defaultValue
*/
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue;
}
下一段代码列举对比了如何在JDK8之前检查一个map中匹配提供键的值是否找到,没找到匹配项就使用一个默认值是如何实现的,并且现在在JDK8中是如何使用的。
/*
* 示范Map.getOrDefault方法并和JDK8之前的实现方法做对比。JDK8
* 中新增的Map.getOrDefault方法相比于传统的实现方法,所用的代码行数更少
* 并且允许用一个final类型的变量来接收返回值。
*/
// JDK8之前的实现方法
String capitalGeorgia = statesAndCapitals.get("Georgia");
if (capitalGeorgia == null)
{
capitalGeorgia = "Unknown";
}
// JDK8的实现方法
final String capitalWisconsin = statesAndCapitals.getOrDefault("Wisconsin", "Unknown");
Map.putIfAbsent(K,V)
Map的新方法putIfAbsent(K,V):若指定的参数中的key所对应的值是null,则将参数中的value的值替换null,并返回value:
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
这在另一段对比JDK8之前的实现方法和JDK8的实现方法的代码示例中得到了证明。
/*
* 示范Map.putIfAbsent方法并和JDK8之前的实现方法做对比。JDK8
* 中新增的Map.putIfAbsent方法相比于传统的实现方法,所用的代码行数更少
* 并且允许用一个final类型的变量来接收返回值。
*/
// JDK8之前的实现方式
String capitalMississippi = statesAndCapitals.get("Mississippi");
if (capitalMississippi == null)
{
capitalMississippi = statesAndCapitals.put("Mississippi", "Jackson");
}
// JDK8的实现方式
final String capitalNewYork = statesAndCapitals.putIfAbsent("New York", "Albany");
Map.remove(Object key, Object value)
Map的新方法remove(Object,Object)超越了长期有效的Map.remove(Object)方法,只有在提供的键和值都匹配的时候才会删除该map项(之前的有效版本只是查找“键”的匹配来删除)。
/**
* 当提供的键和值都匹配的时候才会删除该map项
*/
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
下面这段代码列举展示的是新实现方法和JDK8之前的实现方法的一个具体比较。
/*
* 示范Map.remove(Object,Object)方法并和JDK8之前的实现方法做对比。JDK8
* 中新增的Map.remove(Object,Object)方法相比于传统的实现方法,所用的代码行数更少
* 并且允许用一个final类型的变量来接收返回值。
*/
// JDK8之前的实现方式
boolean removed = false;
if ( statesAndCapitals.containsKey("New Mexico")
&& Objects.equals(statesAndCapitals.get("New Mexico"), "Sante Fe"))
{
statesAndCapitals.remove("New Mexico", "Sante Fe");
removed = true;
}
// JDK8的实现方式
final boolean removedJdk8 = statesAndCapitals.remove("California", "Sacramento");
Map.replace(K,V)
两个新增的Map “replace”方法中的第一个方法只有在指定的键已经存在并且有与之相关的映射值时才会将指定的键映射到指定的值(新值)。
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
下面展示的是新方法和JDK8之前的方法比较:
/*
* 示范Map.replace(K, V)方法并和JDK8之前的实现方法做对比。JDK8
* 中新增的Map.replace(K, V)方法相比于传统的实现方法,所用的代码行数更少
* 并且允许用一个final类型的变量来接收返回值。
*/
// JDK8之前的实现方式
String replacedCapitalCity;
if (statesAndCapitals.containsKey("Alaska"))
{
replacedCapitalCity = statesAndCapitals.put("Alaska", "Juneau");
}
// JDK8的实现方式
final String replacedJdk8City = statesAndCapitals.replace("Alaska", "Juneau");
Map.replace(K,V,V)
第二的新增的Map replace方法在替换现存值方面有更窄的释义范围。当那个方法(上一个replace方法)只是涵盖指定的键在映射中有任意一个有效的值的替换处理,而这个“replace”方法接受一个额外的(第三个)参数,只有在指定的键和值都匹配的情况下才会替换。
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
下面这段代码列举展示的是新实现方法和JDK8之前的实现方法的一个具体比较。
/*
* 示范Map.replace(K, V, V)方法并和JDK8之前的实现方法做对比。JDK8
* 中新增的Map.replace(K, V, V)方法相比于传统的实现方法,所用的代码行数更少
* 并且允许用一个final类型的变量来接收返回值。
*/
// JDK8之前的实现方式
boolean replaced = false;
if ( statesAndCapitals.containsKey("Nevada")
&& Objects.equals(statesAndCapitals.get("Nevada"), "Las Vegas"))
{
statesAndCapitals.put("Nevada", "Carson City");
replaced = true;
}
// JDK8的实现方式
final boolean replacedJdk8 = statesAndCapitals.replace("Nevada", "Las Vegas", "Carson City");
Map的三种遍历
以代码为例:
package char20;
import java.util.*;
/**
* Created by japson on 8/15/2017.
*/
public class TestMap {
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
map.put("AA",123);
map.put("BB",234);
map.put("CC",345);
map.put("CC",111); //key是存在Set里,所以不重复,后put的覆盖之前的
map.put(null,null);
System.out.println(map.size());
//第一种方法:遍历key
Set<String> set = map.keySet(); //set对象指向map的keySet的set
//通过遍历set集合来获取map中的set,通过get()方法获取value
for(Object o : set) {
System.out.print(o + "," + map.get(o) +";");
}
System.out.println();
//第二种方法:通过collection自带的迭代器遍历
Collection<Integer> collection = map.values();
Iterator<Integer> i = collection.iterator();
while (i.hasNext()) {
System.out.print(i.next() + ",");
}
System.out.println();
//第三种方法:通过获取Set中的key来利用get方法获取key 和 value
for(Object o : set) {
System.out.println("<" + o + "," + map.get(o) + ">");
}
Set set1 = map.entrySet(); set1对象指向map的entrySet()
for(Object o : set1) {
Map.Entry entry = (Map.Entry)o; //set1里面存的是Entry,但是之前是Object类型的,所以要强转
System.out.println("<" + entry.getKey() + "," + entry.getValue() + ">");
}
/** 第四种方法(推荐):通过entry.setValue()
* Map.entry<Integer,String>映射项(键-值对),内部有如下方法:
* entry.getKey() ; entry.getValue(); entry.setValue();
* map.entrySet() 返回此映射中包含的映射关系的 Set视图。
*/
for(Map.Entry<String,Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ";" + entry.getValue());
}
}
}
总结
Map 有以下特点:
- 没有重复的 key
- 每个 key 只能对应一个 value, 多个 key 可以对应一个 value
- key,value 都可以是任何引用类型的数据,包括 null
- Map 取代了古老的 Dictionary 抽象类
注意:
可以使用 Map 作为 Map 的值,但禁止使用 Map 作为 Map 的键。因为在这么复杂的 Map 中,equals() 方法和 hashCode() 比较难定义。
另一方面,你应该尽量避免使用“可变”的类作为 Map 的键。如果你将一个对象作为键值并保存在 Map 中,之后又改变了其状态,那么 Map 就会产生混乱,你所保存的值可能丢失。
ps:用心学习,喜欢的话请点赞 (在左侧哦)