手撕Java源码系列之Map接口

Map简介

#Map是以Key和Value进行存储的,这点就是与Collection做一个区别。Map的Key不可重复,但是value可以重复。而Collection最直观的感受就是其子接口Set,仅能存单独的元素,且元素不重复,可以类比Map中的key。

  • Map的存储形式是key-value即键值对形式,可以通过key的值快速的找到value的值。
  • Map中的键值对以Entry类型的对象实例化存在,Entry是Map中的一个静态接口,每次我们去取一个键值对的时候,其实就是在访问每一个Entry对象,这个静态接口定义了各种键值对的操作。
  • 形式中,Key键是不可重复的,Value可重复,一个value值可以和很多个key形成对应关系,一个Key只能对应一个value。
  • 其中K与V的类型是无所谓的。可以是任何类型。
  • Map是一个逻辑结构,如HashMap、TreeMap、Hashtable、LinkedHashMap等。

1. 文档阅读

这个部分想带大家直接的读一下关于这个Map接口的文档,并且加上一些自己的理解。希望能帮助大家手撕源码。这里的源码版本是jdk1.8。因为博主的英文也很一般,仅仅CET-6级水平,加上有道翻译,如有不对还请见谅。

文档中map介绍

  • 在这里插入图片描述map是一个键到值的一个对象,这个map是没有重复键的,每一个键最多映射到一个value值。

  • 在这里插入图片描述
    这个map接口代替了字典类,字典类是一个完全抽象的类,而不是一个接口。

  • 手撕Java源码系列之Map接口_第1张图片
    这个接口(即map)提供了三个集合视图,允许一个map的内容被以一组键、一组值或者一个key-value去看待。如下图所示,因为视图有三种,所以遍历的时候也可以有三种方式(后续会细说)。手撕Java源码系列之Map接口_第2张图片
    map上的order即调用顺序,被定义为一种map集合试图上的迭代器返回其元素的顺序。一些map的实现,比如TreeMap,这些类对顺序做了特点的保证,而HashMap类就没有这样的定义。其实意思就是说对于map这些键的遍历,一般是以迭代器为标准的。但是有的类比如TreeMap,它就有自己的定义,而HashMap就没有。

  • 手撕Java源码系列之Map接口_第3张图片
    注意:(这里是关于可不可变的问题)如果把可变对象作为map的键,那就要引起注意了。如果一个对象的值,影响到了比较函数,并且这个对象还是一个键,那么这个映射行为是不被指定的。(意思就说是,如果保存的key是可变对象,比如MutableKey,那么在改变了它的对象值时,它的hashcode就改变了。那后续去查的时候就查不到了。)然后在这个禁止的行为中有一个比较特殊的就是map中的对象包含自己,把map作为一个对象。尽管把自己作为值是没问题的,但是使用注意的是:equals和hashCode方法就不再能够在map中很好的定义了。
    这段的总结就是,不要用可变对象作为key值,多用String,Integer这类不可变对象作为key,还有一点就是少用map作为对象,否则equeal和hashcode方法不好定义。

  • 手撕Java源码系列之Map接口_第4张图片
    所有的通用map的实现类都应该提供两个标准的构造方法:一个是无参构造,能创建一个空的map,另一个是map类型的单参数构造器,这个构造法能够创建一个新的map,然后值和原本的那个map一致,(其实就是做一个映射)。实际上,第二个构造器允许用户去复制任何映射,生成所需类的等效映射。上面这个建议没办法强制遵守(因为如果是某接口实现map接口,那没辙,因为接口没有构造方法)但是JDK中的所有通用映射都实遵循了这个建议。

  • 手撕Java源码系列之Map接口_第5张图片
    在这个接口中存在“破坏性”方法,即在操作时能够去修改map的方法。如果这个map不支持这个操作,那么“破坏性”方法就需要指定的抛出UnsupportedOperationException。如果出现了没定义的这种情况,但是如果用户发出一个调用对map无影响,然后这方法可能就会抛出UnsupportedOperationException异常,但是这不是必要的。举个例子,在一个不可修改的映射上调用了putAll(map)方法可能会抛出异常,如果map要“叠加”其映射的映射是空的,那就不是必须这样做。

  • 手撕Java源码系列之Map接口_第6张图片
    一些map接口的实现类,对它们可能包含的键与值会有限制。举个例子,一些实现类禁止空的键和空的值,而有些实现类对键的类型有限制。企图去插入一个不合法的键值对会抛出未检查的异常,传统上就是空指针异常(NullPointerException)或者ClassCastException异常。试图去查询一个不符合条件的键值对也可能抛出异常,或者返回一个简单的false。一些实现类可能会采取前一种展示的行为,一些可能会是后面一种。更普遍的是,当你试图去操作一个不符合条件的键值对,此键值对的实现不会导致将不符合条件的元素插入到映map中,那这个操作可能会抛出异常或者也可能成功,这根据实现的选项中选择。这种异常在接口的规范中被标记为“可选”。

  • 手撕Java源码系列之Map接口_第7张图片
    在集合框架接口中的许多方法用equals的方法定义。比如,在containsKey(Object)方法的规范中提到:如果当且仅当这个map中包含一个key的映射时,返回true,语法就是(keynull ? knull : key.equals(k)),判断key是不是空,是空那么null,不是就是调用equals方法去传入k。该规范不应被解释为暗示调用带有非空参数键的Map.containsKey(),而是将会引起key.equals(k)被调用。因为任何关键k实现都可以自由地实现优化,从而避免equals的调用。举个例子,首先比较两个key的hashcodes值,hashCode()方法的规范确保了这两个对象没有相等的hashcode。更一般的说,各种集合框架接口的实现类是很自由的去利用底层方法指定的行为,只要开发者觉得合适就行。

  • 手撕Java源码系列之Map接口_第8张图片
    一些map操作对于map执行递归遍历可能会失败,在map直接或间接包含自身的自引用例子中,会随之而来意外。就比如clone、equals、hashcode和toString方法,实现类可能选择性的处理自引用场景,但是目前大多数实现类都没有这样做。

  • 在这里插入图片描述
    这个接口是java的集合框架中的成员。

  • 在这里插入图片描述
    参数是由map维护的键类型,参数是被映射的值。

  • 手撕Java源码系列之Map接口_第9张图片
    作者是约书亚·布洛克,这个接口的实现类有HashMap、TreeMap、Hashtable、SorteMap、Collection、Set。然后从1.2版本开始有的这个接口。

接口的方法设计(map信息查询)

1. int size()

    int size();

返回这个集合中的key-value数量,如果键值对超过了Integer类型的最大值,那就返回Integer类型的最大值。
2. boolean isEmpty()

    boolean isEmpty();

判空操作,如果这个map集合中没有key-value映射,那就是true,否则false。
3.boolean containsKey(Object key)

    boolean containsKey(Object key);

用key来做索引判断,如果map中存在这个key那就返回true。但是如果说这个键的类型不适合应该映射那就抛出异常。这就是之前map文档注意的,有的实现类对键的类型有要求。

4.boolean containsValue(Object value)

    boolean containsValue(Object value);

用value来做索引判断,如果map中存在这个value那就返回true。如果这个value值如果不符合返回值类型的设定,那就是抛ClassCastException异常。如果这个value值是空,并且实现类map不允许空的话,那就是抛出NullPointerException异常。
5.V get(Object key)

    V get(Object key);

通过这个方法,可以传入key值,从而获得value值。如果map没有这个key的映射,那就返回空。
文档的其他内容就是关于返回值空不空的问题,有的类会限制是否可以为空值。英文文档在上述,内容比较好理解,这里不赘述。
参数就是key值,返回值就是与key相关联的value或者null。如果类型不符合,那就抛出ClassCastException,如果是空值,并且这个实现类不允许空,那么抛出NullPointerException

接口的方法设计(map中映射修改)

6.V put(K key, V value);

    V put(K key, V value);

插入操作,向map集合中插入键值对。如果说是原本map中不存在那么可以直接添加这个映射,如果是原本存在,那么会替换旧值。
文档中有四种异常的抛出。
UnsupportedOperationException:如果put操作再实现map的类中不被支持,那么旧抛出这个异常。
ClassCastException:如果如果这组key和value的类不被允许插入到map中,则会抛出这个异常。
NullPointerException:如果key或者value的值是空的,并且这个实现类是不允许空值的,那么就会抛出这个异常。
IllegalArgumentException:如果指定的键或者值的某些属性被阻止存储进去,那么就会报非法参数。
注意区分ClassCastException和IllegalArgumentException,一个是因为类不被允许,一个是属性不被允许。
7.V remove(Object key);

V remove(Object key);

如果一个键存在,则从这个映射中移除该键值对,更一般的说判断这个key在不在,如果key存在,那么该映射就会被删除。如果映射不存在,那么就会返回null,但是如果这个实现类是允许空值的,那可能存的就是空值。
可能抛出的三个异常:
UnsupportedOperationException:如果这个remove操作在map中不被支持,那么则会抛出这异常。
ClassCastException:如果key的类型是不合理对于这个map,则会抛出这个异常。
NullPointerException:如果key是空的或者这个map是不允许空值的那么就会报错。
8.void putAll(Map m);

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

这个方法可以做一个复制操作,输入的参数是一个map,可以将所有的映射从指定的映射复制到此映射中。这个操作相当于对每个key做了一个put。
输入参数:需要被映射的map,也包含四个异常,如UnsupportedOperationException、ClassCastException、NullPointerException、IllegalArgumentException。四个异常的描述和其他几个方法一致。
9.void clear();

 void clear();

调用这个方法,清除map里的所有映射关系。异常的报错只包含UnsupportedOperationException,即实现类不支持此方法则报错。

接口的方法设计(key-value的查看方式)

10.Set keySet()

  Set<K> keySet();

在前面map文档的批注中,有三种视图,一个从key角度,一个从value角度,另一个从key-value角度。因此这个keySet()方法就是从key的角度进行查看。调用这个方法,就能够返回一个Set集合,其中的元素就是保存的key对象。但是Set其实是由Map支持的,所以其实就是对map的更改会反映在集合中,反之亦然。如果在对集合进行迭代的时修改了映射,那么迭代的这个顺序结果是未定的,即需要实现类自己去规定。这个key值得集合支持删除,不支持添加,并且Set中的元素是不可重复的。
** 11. Collection values(); **

   Collection<V> values();

同理,这个是从值得角度查看的,Collection集合是支持重复值的。集合是映射支持的,所以对映射的更改会反映在集合中,反之亦然。如果在集合中进行迭代,除了迭代器自己的remove操作,其他的结果都是未定义的,就比如遍历的结果顺序是不定的。value值得这个集合也是支持删除,不支持添加。
12.Set> entrySet();

Set<Map.Entry<K, V>> entrySet();

第三个视图是从key-value的角度进行遍历的,把一个key-value的映射关系作为一个对象进行封装。然后把这个对象以Set集合进行返回。同理与SetKey方法,只是集合中的内容不同。

封装映射关系的Entry接口

entry是内部接口,其保存的是映射关系(key与value)的关系。上述的entrySet()方法返回的集合就是这个类。获取一个个map项的唯一方法就是用集合视图迭代器。这些视图在迭代器存在期间是有效的,更正式的说一个映射项entry,在迭代器返回后修改了后台映射,那么映射项的行为是未定义的,除非通过对映射项执行setValue操作。
###entry接口中提供的方法如下
1. K getKey();

 K getKey();

这个接口的entry是包含了key和value的,所以通过getKey()方法即可获得映射关系中的key。
异常:IllegalStateException,即接口可能会抛出这个异常,由于这个entry已经被删除了,但是这个操作不是刚需。
2.V getValue();

V getValue();

相对的,这个方法就是获得映射关系中的value值。异常也是IllegalStateException,即如果这个映射没了。那就得不到,就报错。同理这个也不是必须实现的。

3. V setValue(V value);

V setValue(V value);

这个方法作用是去更改值的,即改变原本映射中的value,写入原本的映射中。但是如果这个映射已经被删除了,那么这个调用的结果就没定义了。(通过迭代器删除的)
参数就是新的value,然后会返回旧的value值。
异常共有5个:
UnsupportedOperationException:后台如果不支持put操作,那就表明不支持这个方法。由此也可以看出替换value值的方法,在实现类中可能是通过put方法执行的。
ClassCastException:如果要存储的这个value类不被支持,那么就报这个错。
IllegalArgumentException:如果被存储的这个value的属性值不被支持,那么就是这个错。
NullPointerException:如果备份映射不允许空值,但指定了空值那就报错。
IllegalStateException:这个错误原因就是如果映射都被删除了,那就存不进去,就报错,但是这个不是刚需。
4.boolean equals(Object o);

boolean equals(Object o);

此方法就是对比两个entry是否是同一个,如果是那就是true。更细致的说就是如果两个key都不是空,那么对比下是不是key相同,然后再对比value,通过getvalue值,确保两个都不是空,并且还相等,那么就是相同的entry。这样就确保了在不同接口中实现map.entry还能让equals方法生效。
5.int hashCode();

int hashCode();

返回的一个对应这个entry的哈希编码。这个哈希编码是由key的哈希和value的哈希功能构成的。这就确保了上一个euqals方法到底比的是什么。实质上比的是哈希值。
6.public static , V> Comparator> comparingByKey()

public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

这个方法就是返回一个比较器,比较器的比较是按key的自然顺序。比较器是可被序列化的,当在比较空的时候会返回空指针异常。
7. public static > Comparator> comparingByValue()

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的比较那么就有value的比较,返回一个value自然顺序的构造器。同样也是可被序列化,如果是null则会报空指针异常。
8.public static Comparator> comparingByKey(Comparator cmp)

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());
        }

这个方法可以与前面比较key的方法做一个区别,上一个key方法是用自然顺序进行返回的,但是这个是需要你传入一个比较器,通过这个比较器进行排序,从而对两个key进行对比。
9.public static Comparator> comparingByValue(Comparator cmp)

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());
        }

同理这个方法也是传入一个比较器对value值进行对比。

接口的方法设计(比较与hashing)

1.boolean equals(Object o);

boolean equals(Object o);

对两个对象进行比较,更准确的说在map中就是比较key 和value会不会同时都相等,如果会的话那么就返回true,否则false。实质背后调用的是内部接口entry中的equals方法,比较的也是entry中的key与value。这样确保了不同的实现类比较的东西是相同的。

2.int hashCode();

int hashCode();

返回此map的哈希编码。map的哈希值被定义为每一个entry的entrySet()中的值的和。提到Set实际就是每一个key的哈希编码进行加和就是这个map的哈希值。

接口的方法设计(默认方法)

其实在一开始我都以为接口是只能写抽象方法的,不能有方法体,但是在1.8default关键字打破了这个规矩,即接口中能够定义具体的方法了。如果接口不想修改或者重写这个方法,那么实现了这个接口的类可以直接调用。
**1.default V getOrDefault(Object key, V defaultValue) **

default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }

输入key,然后返回这个key映射的value,但是如果说这个映射不存在value值,那么就会返回一个默认定义的defaultValue。
2.default void forEach(BiConsumer action)

default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

//未完待续

你可能感兴趣的:(java学习,java)