博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java集合中的AbstractMap抽象类
阅读量:5908 次
发布时间:2019-06-19

本文共 6078 字,大约阅读时间需要 20 分钟。

jdk1.8.0_144

AbstractMap抽象类实现了一些简单且通用的方法,本身并不难。但在这个抽象类中有两个方法非常值得关注,keySet和values方法源码的实现可以说是教科书式的典范。

  抽象类通常作为一种骨架实现,为各自子类实现公共的方法。上一篇我们讲解了Map接口,此篇对AbstractMap抽象类进行剖析研究。

  Java中Map类型的数据结构有相当多,AbstractMap作为它们的骨架实现实现了Map接口部分方法,也就是说为它的子类各种Map提供了公共的方法,没有实现的方法各种Map可能有所不同。

  抽象类不能通过new关键字直接创建抽象类的实例,但它可以有构造方法。AbstractMap提供了一个protected修饰的无参构造方法,意味着只有它的子类才能访问(当然它本身就是一个抽象类,其他类也不能直接对其实例化),也就是说只有它的子类才能调用这个无参的构造方法。

  在Map接口中其内部定义了一个Entry接口,这个接口是Map映射的内部实现用于维护一个key-value键值对,key-value存储在这个Map.Entry中。AbstractMap对这个内部接口进行了实现,一共有两个:一个是可变的SimpleEntry和一个是不可变的SimpleImmutableEntry。

public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable

  实现了Map.Entry<K, V>接口,并且实现了Serializable(可被序列化)。

  它的方法比较简单都是取值存值的操作,对于key值的定义是一个final修饰意味着是一个不可变的引用。另外其setValue方法稍微特殊,存入value值返回的并不是存入的值,而是返回的以前的旧值。需要重点学习的是它重写的equals和hashCode方法。

1 public boolean equals(Object o) {2     if (!(o instanceof Map.Entry))        //判断参数是否是Map.Entry类型,要equals相等首先得是同一个类型3         return false;4     Map.Entry
e = (Map.Entry
)o; //将Object类型强转为Map.Entry类型,这里参数使用“?”而不是“K, V”是因为泛型在运行时类型会被擦除,编译器不知道具体的K,V是什么类型5 return eq(key, e.getKey()) && eq(value, e.getValue()); //key和value分别调用eq方法进行判断,都返回ture时equals才相等。6 }
1 private static boolean eq(Object o1, Object o2) {2     return o1 == null ? o2 == null : o1.equals(o2);    //这个三目运算符也很简单,只不过需要注意的是尽管这里o1、o2是Object类型,Object类型的equals方法是通过“==”比较的引用,所以不要认为这里有问题,因为在实际中,o1类型有可能是String,尽管被转为了Object,所以此时在调用equals方法时还是调用的String#equals方法。3 }

  要想正确重写equals方法并能正确使用,通常还需要重写hashCode方法。

1 public int hashCode() {2     return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());    //key和value的值不为null时,将它们的hashCode进行异或运算。3 }

public static class SimpleImmutableEntry<K,V> implements Entry<K,V>, java.io.Serializable SimpleImmutableEntry

  定义为不可变的Entry,其实是事实不可变,因为它不提供setValue方法,在多个线程同时访问时自然不能通过setValue方法进行修改。它相比于SimpleEntry其key和value成员变量都被定义为了final类型。调用setValue方法将会抛出UnsupportedOperationException异常。

   它的equals和hashCode方法和SimpleEntry一致。

  接下来查看AbstractMap抽象类实现了哪些Map接口中的方法。

public int size()

  Map中定义了一个entrySet方法,返回的是Map.Entry的Set集合,直接调用Set集合的size方法即是Map的大小。

public boolean isEmpty()

  调用上面的size方法,等于0即为空。

public boolean containsKey(Object key)

  这个方法的实现较为简单,通过调用entrySet方法获取Set集合的迭代器遍历Map.Entry,与参数key比较。Map可以存储为null的key值,由于key=null在Map中存储比较特殊(不能计算hashCode值),所以在这里也做了判断参数key是否为空。

public boolean containsValue(Object value)

  这个方法实现和containsKey一致。

public V get(Object key)

  这个方法实现和上面两个也类似,不同的是上面相等返回boolean,这个方法返回value值。

public V put(K key, V value)

  向Map中存入key-value键值对的方法并没有具体实现,会直接抛出一个UnsupportedOperationException异常。

public V remove(Object key)

  通过参数key删除Map中指定的key-value键值对。这个方法也很简单,也是通过迭代器遍历Map.Entry的Set集合,找到对应key值,通过调用Iterator#remove方法删除Map.Entry。

public void putAll(Map<? extends K, ? extends V> m)

  这个方法也很简单遍历传入的Map,调用put方法存入就可以了。

public void clear()

  调用entrySet方法获取Set集合再调用Set#clear()方法清空。

public Set<K> keySet()

  返回Map key值的Set集合。AbstractMap中定义了一个成员变量“transient Set<K> keySet”,在JDK7中keySet变量是由volatile修饰的,但在JDK8中并没有使用volatile修饰。在对keySet变量的注释中解释道,访问这些字段的方法本身就没有同步,加上volatile也不能保证线程安全。关于keySet方法的实现就有点意思了。

  首先思考该方法是返回key值的Set集合,很自然的能想到一个简单的实现方式,遍历Entry数组取出key值放到Set集合中,类似下面代码:

1 public Set
keySet() {2 Set
ks = null;3 for (Map.Entry
entry : entrySet()) {4 ks.add(entry.getKey());5 }6 return ks;7 }

  这就意味着每次调用keySet方法都会遍历Entry数组,数据量大时效率会大大降低。不得不说JDK源码是写得非常好,它并没有采取遍历的方式。如果不遍历Entry,那又如何知道此时Map新增了一个key-value键值对呢?

  答案就是在keySet方法内部重新实现了一个新的自定义Set集合,在这个自定义Set集合中又重写了iterator方法,这里是关键,iterator方法返回Iterator接口,而在这里又重新实现了Iterator迭代器,通过调用entrySet方法再调用它的iterator方法。下面结合代码来分析:

1 public Set
keySet() { 2 Set
ks = keySet; //定义的transient Set
keySet 3 if (ks == null) { //第一次调用肯定为null,则通过下面代码创建一个Set示例 4 ks = new AbstractSet
() { //创建一个自定义Set 5 public Iterator
iterator() { //重写Set集合的iterator方法 6 return new Iterator
() { //重新实现Iterator接口 7 private Iterator
> i = entrySet().iterator(); //引用Entry的Set集合Iterator迭代器 8 9 public boolean hasNext() {10 return i.hasNext(); //对key值的判断,就是对entry的判断11 }12 13 public K next() {14 return i.next().getKey(); //取下一个key值,就是取entry#getKey15 }16 17 public void remove() {18 i.remove(); //删除key值,就是删除entry19 }20 };21 }22 23 public int size() { //重写的Set#size方法24 return AbstractMap.this.size(); //key值有多少就是整个Map有多大,所以调用本类的size方法即可。这个是内部类,直接使用this关键字代表这个类,应该指明是调用AbstractMap中的size方法,没有this则表示是static静态方法25 }26 27 public boolean isEmpty() { //重写的Set#isEmpty方法28 return AbstractMap.this.isEmpty(); //对是否有key值,就是判断Map是否为空,,所以调用本类的isEmpty方法即可29 }30 31 public void clear() { //重写的Set#clear方法32 AbstractMap.this.clear(); //清空key值,就是清空Map,,所以调用本类的clear方法即可33 }34 35 public boolean contains(Object k) { //重写Set#contains方法36 return AbstractMap.this.containsKey(k); //判断Set是否包含数据k,就是判断Map中是否包含key值,所以调用本类的containsKey方法即可37 }38 };39 keySet = ks; //将这个自定义Set集合赋值给变量keySet,在以后再次调用keySet方法时,因为keySet不为null,只需直接返回。40 }41 return ks;

  我认为这是一种很巧妙的实现,尽管这个方法是围绕key值,但实际上可以结合Entry来实现,而不用遍历Entry,同时上面提到了调用entrySet# iterator方法,这里则又是模板方法模式的最佳实践。因为entrySet在AbstractMap中并未实现,而是交给了它的子类去完成,但是对于keySet方法却可以对它进行一个“算法骨架” 实现,这就是模板方法模式。

public Collection<V> values()

  对于values方法则完全可以参考keySet,两者有着异曲同工之妙,这里为节省篇幅不再赘述。

public abstract Set<Entry<K,V>> entrySet()

  一个抽象方法,交给它的子类去完成,说明这个方法并不是特别“通用”。

public boolean equals(Object o)

  Map中规定只有在Map中的每对key-value键值对的key和value都一一对应时他们的equals比较才返回true。在方法中先判断简单的条件,如果引用相等,直接返回true,如果参数o不是Map类型直接返回false,如果两个Map的数量不同也直接返回false。后面才再遍历Entry数组比较Entry中的key和value是否一一对应。方法简单,但这给了我们一个启示,在条件判断中,先判断简单的基本的,再判断复杂的。

public int hashCode()

  重写了Object类的equals方法,重写hashCode也是必须的。AbstractMap对hashCode的实现是将所有Map.Entry(这里就是SimpleEntry或SimpleImmutableEntry)的hashCode值向加,最后得出的总和作为Map的hashCode值。

public String toString()

  这个方法没什么好说的,就是取出所有键值对使用StringBuilder对其进行拼接。

protected Object clone() throws CloneNotSupportedException

  实现一个浅拷贝,由于是浅拷贝对于变量keySet和values不进行拷贝,防止两个浅拷贝引发的问题,关于Object中的clone方法在已有解析。

 

 

这是一个能给程序员加buff的公众号 

转载地址:http://yuvpx.baihongyu.com/

你可能感兴趣的文章
说说设计模式~ 观察者模式与消费者模式的区别
查看>>
封装的关键字
查看>>
hdu 4006 The kth great number SBT
查看>>
斐波那契数列及青蛙跳台阶问题
查看>>
Vue常见组件
查看>>
如何避免重构带来的危险
查看>>
IOS创建开源库步骤,提交cocoa pods官网,别人可以使用
查看>>
退出游戏,重新开始的方法
查看>>
Aizu 2304 Reverse Roads(无向流)
查看>>
kali 2.0 启动metasploit服务
查看>>
小程序生命周期
查看>>
【转】用图片做列的背景图片,出现背景图片的重复?
查看>>
开源系统监控软件
查看>>
nginx 在浏览器端保持cookie 一致
查看>>
【atcoder】All Your Paths are Different Lengths[arc102D](乱搞)
查看>>
Error: Cannot retrieve metalink for repository: epel. Please verify its path and try again
查看>>
zabbix使用自动发现监控esxi的磁盘存储storage
查看>>
linux服务器系统盘坏且系统盘为软raid的修复方法
查看>>
CentOS6.5实现rsync+inotify实时同步
查看>>
Xstream序列化实体
查看>>