Java集合学习之Map

Map

Posted by Jerry on April 5, 2017

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

Map

Map是一个接口,一个map不能包含重复的Key,每个key只能映射一个唯一的value。Map接口是用来取代Dictionary抽象类的。

Map接口提供三个集合视图:

  • key的集合
  • value的集合
  • key-value的集合

Map内元素的顺序取决于Iterator的具体实现,获取集合视图其实是获取一个迭代器,实现对遍历元素细节的隐藏。TreeMap类能保证遍历元素的顺序,而HashMap就无法保证遍历元素的顺序。

注意:当使用可变对象作为map的key 值时要小心,如果对象的值改变影响了equals()方法的对比值,那么map的映射是不具体的(可能会映射到错误的value上去)。需要禁止的是不应当用map类来作为key。虽然可以用map类作为value,但建议您非常小心:在这样的map上,equals和hashCode方法已经不再很好地定义了(换言之,就是别拿map类作为key和value)。

所有多种用途的map实现类应该提供两个“标准”构造器,一个无参构造器用来创建一个空map,一个只有一个参数,参数类型是map的构造器,用来创建一个新的和传入参数有一样key-value映射的map。实际上,后者允许复制任何一个map,这仅仅是一个建议,并没有强制要求,因为接口是无法包含构造器的,不过这个建议在JDK被遵守。

map接口内“破坏性”方法即修改map对象的那些方法,如果进行非法操作,那么抛出UnsupportedOpeartionException。例如putAll(Map)方法,如果传入的值为空那么就抛出UnsupportedOpeartionException异常。

一些实现了Map接口的类会对Map的Key和Value值做出限制,如不允许为null,还有的现在它们Key的类型。试图插入非法的(ineligible)Key或者Value是,将抛出一个unchecked的异常,典型的有NullPointerExceptionClassCastException.

Map接口的几个实现主要有HashMap, TreeMap, Hashtable, SortedMap四个Map实现类,如下: (1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。

Map接口内的方法

1.7的方法

int size();//返回map中key-value映射的数量

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

//根据key得到对应的value,如果没有对应的映射,返回null,如果map允许value为null,返回
//null可能是有一对key-null的映射或没有对应的映射
V get(Object key);

//往map放入一对key-value映射
V put(K key, V value);

//根据key删除对应映射
V remove(Object key);

//复制一份与参数一样的map
void putAll(Map<? extends K, ? extends V> m);

//清空map中所有的映射
void clear();

//返回map中所有key的集合
Set<K> keySet();

//返回map中所有value的集合
Collection<V> values();

//返回key-value的集合
Set<Map.Entry<K, V>> entrySet();

//比较调用者与参数是否相等
boolean equals(Object o);

//计算map的hash code
int hashCode();

//还有其他default方法...,都是jdk1.8发布的

java1.8方法(注意Java8中多了几个默认方法)

    //和get()方法类似如果map中有key,那么返回对应value,否则返回defaultValue
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
    //好像遍历所有(Key,Value),用action.accpt处理(Key,Value)
    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);
        }
    }

内部接口Entry<K,V>

Map接口里有一个内部接口Entry<K,V>,其实它就是Map存放key-value映射的数据结构,其中接口内的后几个比较的方法是1.8后才有的。

interface Entry<K,V> {
    
  //返回对应的key
    K getKey();

  //返回对应的value
    V getValue();
    
  //设置用新value替换旧value,返回值是旧value
    V setValue(V value);

  //如果两个entry的映射一样,返回true
    boolean equals(Object o);

  //计算entry的hash code
    int hashCode();

  //下面的静态方法是JDK1.8才发布的
  //返回一个比较器,比较的规则是key的自然大小
    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());//这里用的是lambda表达式
        }
  
  
  //返回一个比较器,比较规则是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());
        }
}

HashMap

HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,HashMap解决冲突是用链表来实现的,当某个哈希值冲突的个数超过一定个数时,那么就用树来处理冲突值(方便快速查找)。TreeMap就是用树来解决。其实是红黑树

HashMap的数据结构

1.HashMap的Node类的代码

/** Node是单向链表。    
     * 它是 “HashMap链式存储法”对应的链表。    
     *它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数  
    **/  
    static class Node<K,V> implements Map.Entry<K,V> {    
        final K key;    
        V value;    
        // 指向下一个节点    
        Node<K,V> next;    
        final int hash;    
   
        // 构造函数。    
        // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"    
        Node(int h, K k, V v, Node<K,V> n) {    
            value = v;    
            next = n;    
            key = k;    
            hash = h;    
        }    
   
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        //key和value的哈希值的异或
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        // 判断两个Entry是否相等    
        // 若两个Entry的“key”和“value”都相等,则返回true。    
        // 否则,返回false    
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }     
    }

2.HashMap类属性值

//table初始化,是在put()调用是进行初始化,而不是在new的时候初始化。
transient Node[] table;//存储元素的实体数组
 
transient int size;//存放元素的个数
 
int threshold; //// 所能容纳的key-value对临界值(阙值) 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量

final float loadFactor; //加载因子,loadFactor加载因子是表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)冲突的机会越大,则查找的成本越高.默认为0.75。因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
transient int modCount;//被修改的次数,List里面也有这个,一般是在使用迭代器时候,对迭代的时候进行修改抛异常

3.构造方法

    //默认容量(长度)大小为16,必须是2的次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大为2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final int TREEIFY_THRESHOLD = 8;
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    //找初始化时,恰好比cap大的2的幂的值
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

4.hash()方法确定哈希桶数组索引位置

HashMap中的数据结构是数组+单链表的组合,希望的是元素存放的更均匀,最理想的效果是,数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。那如何计算才会分布最均匀呢?我们首先想到的就是%运算,哈希值%容量=bucketIndex,jdk1.8和1.7实现方式不一样,如下:(这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

//方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h »> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

5. put(Key, Value)方法

注意put()方法有个返回值,返回的是如果存在覆盖之前的key,那么返回之前的value,否则范围null

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断当前哈希表数组是否为空,为空则新建数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //接着判断当前hash值是否有冲突
        //这时就体现map长度为2的次方的优势了,直接做位运算就可以取模
        if ((p = tab[i = (n - 1) & hash]) == null)
            //没有冲突,直接插入到table
            tab[i] = newNode(hash, key, value, null);
        else {
            //否则有冲突
            Node<K,V> e; K k;
            //说明和当前的key值相同,那么直接覆盖当前的node节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断当前的节点是否是TreeNode,如果是的话就是用树来存储冲突值
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //不是TreeNode的话,那就是用链表来存储冲突值
            else {
                //遍历过程中判断是否有重复的key,有的话,那么就覆盖当前key值,
                //遍历到末尾就直接添加上去,接着判断是否当前的链表个数是不是超过一定值,超过了就转换为树
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断当前插入后是否超过一定的值,超过了就转换为树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //超过最大容量,就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

6. 扩容机制

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。具体扩容请看

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //判断当前的table是否为空,即原来的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //判断当前的table是否为空
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //原来的hash表为非空的话,那么就需要复制到新的hash表中去,hash值需要改变一些。
        if (oldTab != null) {
            // 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6.get()和containsKey()方法

首先从hash表中查找(直接根据hash值映射),然后再比较是否key相等(可能有冲突的情况),相等则返回。

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

7.containsValue()方法

这时查找效率比较低,一个个去查。

    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

Java8中的接口中默认方法和默认静态方法

Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。它允许添加新方法到已有接口中,但是不会破坏那些基于老版接口实现的代码的二进制兼容性。默认方法和抽象方法的区别在于:抽象方法是必须要实现的,而默认方法不是。具体默认方法见官网点这里

private interface Defaulable {    
// jdk8中允许接口实现默认方法,你可以重写默认方法,也可以不重写
default String notRequired() {        
    return "Default implementation";    
    }       
}
private static class DefaultableImpl implements Defaulable {}
private static class OverridableImpl implements Defaulable {    
    @Override    
    public String notRequired() {        
        return "Overridden implementation";    
    }

接口Defaulable在定义中使用关键字default声明了一个默认方法notRequired()。有一个类DefaultableImpl实现了这个接口,它没有对默认方法做任何更改。另一个类OverridableImpl,则重写了接口中的默认实现,实现了自己的逻辑。 Java 8提供的另一个很有意思的特性是接口可以声明静态方法(同时提供实现)。举个例子:

private interface DefaulableFactory {    
// Interfaces now allow static methods    
static Defaulable create( Supplier< Defaulable > supplier ) {        
    return supplier.get();   
 }
}

下面这段代码将之前例子中提到的默认方法和静态方法放在了一起:

public static void main( String[] args ) {   
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );    
    System.out.println( defaulable.notRequired() );    
    defaulable = DefaulableFactory.create( OverridableImpl::new );    
    System.out.println( defaulable.notRequired() );
}

这个程序的控制台输出如下:

Default implementation
Overridden implementation

JVM中默认方法的实现非常高效,而且方法调用的字节码指令支持默认方法。默认方法使得已有的Java接口能够改进而不会导致编译过程失败。接口java.util.Collection中新增了大量的方法,都是很好的示例,如: stream(), parallelStream(),forEach(), removeIf(),等等。

虽然默认方法很强大,我们还是要谨慎使用它:在将一个方法定义为default方法之前,最好三思是不是必须要这么做,因为它可能在层级复杂的情况下引起歧义和编译错误。

参考文献

  1. http://www.cnblogs.com/13jhzeng/p/5560676.html
  2. http://www.zicheng.net/article/79.htm
  3. http://www.cnblogs.com/ITtangtang/p/3948406.html
  4. http://www.importnew.com/20386.html