欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Java中AbstractMap抽象类

程序员文章站 2022-03-03 08:05:35
jdk1.8.0_144 下载地址: abstractmap抽象类实现了一些简单且通用的方法,本身并不难。但在这个抽象类中有两个方法非常值得关注,keyset和value...

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方法。

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

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

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

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集合中,类似下面代码:

public set<k> keyset() {
  set<k> ks = null;
  for (map.entry<k, v> entry : entryset()) {
    ks.add(entry.getkey());
  }
  return ks;
}

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

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

public set<k> keyset() {
  set<k> ks = keyset;    //定义的transient set<k> keyset
  if (ks == null) {    //第一次调用肯定为null,则通过下面代码创建一个set示例
    ks = new abstractset<k>() {    //创建一个自定义set
      public iterator<k> iterator() {    //重写set集合的iterator方法
        return new iterator<k>() {  //重新实现iterator接口
          private iterator<entry<k,v>> i = entryset().iterator();  //引用entry的set集合iterator迭代器

          public boolean hasnext() {
            return i.hasnext();    //对key值的判断,就是对entry的判断
          }

          public k next() {
            return i.next().getkey();  //取下一个key值,就是取entry#getkey
          }

          public void remove() {
            i.remove();  //删除key值,就是删除entry
          }
        };
      }

      public int size() {  //重写的set#size方法
        return abstractmap.this.size();  //key值有多少就是整个map有多大,所以调用本类的size方法即可。这个是内部类,直接使用this关键字代表这个类,应该指明是调用abstractmap中的size方法,没有this则表示是static静态方法
      }

      public boolean isempty() {  //重写的set#isempty方法
        return abstractmap.this.isempty();  //对是否有key值,就是判断map是否为空,,所以调用本类的isempty方法即可
      }

      public void clear() {    //重写的set#clear方法
        abstractmap.this.clear();  //清空key值,就是清空map,,所以调用本类的clear方法即可
      }

      public boolean contains(object k) {  //重写set#contains方法
        return abstractmap.this.containskey(k);  //判断set是否包含数据k,就是判断map中是否包含key值,所以调用本类的containskey方法即可
      }
    };
    keyset = ks;  //将这个自定义set集合赋值给变量keyset,在以后再次调用keyset方法时,因为keyset不为null,只需直接返回。
  }
  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不进行拷贝,防止两个浅拷贝引发的问题。