Java 中的集合框架之 Map接口、HashMap 类、HashTable 类、TreeMap 类(1万字超全详解)

一、集合的框架体系

Java 集合框架提供了一套性能优良,使用方便的接口和类,其位于 java.util 包中, 所以当使用集合框架的时候需要进行导包。
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合;另一种是图(Map),存储键/值对映射。

  • 集合(Collection)体系如下:
    集合(Collection)体系图
  • 图(Map)体系如下:

说明:
(1)Collection 接口有两个重要的子接口 List、 Set , 他们的实现子类都是单列集合。
(2)Map 接口的实现子类是双列集合,存放的 K-V(键值对)

1. 常用集合接口概述

如下表:

接口 描述
Collection 接口 Collection 是最基本的集合接口,一个 Collection 代表一组 Object(即 Collection 的元素), Java不提供直接继承自 Collection的类,只提供继承自 Collection 接口的子接口(如 List和 set)。Collection 接口存储一组不唯一,无序的对象(不能通过索引来访问 Collection 集合中的对象)。
List 接口 List 接口继承自 Collection 接口 ,但 List 接口 是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(即元素在 List 中的位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0。而且 List 集合中允许有相同的元素。可以说,List 接口的集合存储一组不唯一,有序(插入顺序)的对象。
Set 接口 Set 接口继承自 Collection 接口,具有与 Collection 完全一样的接口,只是方法上有部分不同,和 Collection 接口 相同,Set 接口存储一组唯一,无序的对象。
Map 接口 Map 接口与 Collection 接口同级(彼此没有继承关系),Map 图存储一组 键-值 对象,提供key(键)到value(值)的映射。

Set 和 List 接口的区别:

(1)Set 接口集合存储的是无序的,不重复的数据。List 接口集合存储的是有序的,可以重复的元素。

(2)Set 集合 底层使用的是 链表数据结构,其检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 (实现子类有 HashSet , TreeSet 等)。

(3)List 结合 底层和数组类似,但是它可以动态增长,根据实际存储的数据的长度自动增长 List 的长度。其检索元素效率高,插入和删除效率低,插入和删除会引起其他元素位置改变 (实现子类有 ArrayList , LinkedList , Vector 等)。

2. 常用 Collection 集合的实现子类

Java 提供了一套实现了 Collection 接口的标准集合类。其中一些是具体类,这些类可以直接拿来使用,而另外一些是抽象类,提供了接口的部分实现。

如下表:

类名 描述
ArrayList 类 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素。该类实现了可变大小的数组,随机访问和遍历元素时,提供了更好的性能。该类是非同步的, 在多线程的情况下不要使用。ArrayList 类在扩容时会扩容当前容量的1.5倍。
Vector 类 该类和 ArrayList 类非常相似,但该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。
LinkedList 类 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素,主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个 LinkedList,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候再构造一个同步的 LinkedList 。
HashSet 类 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。
TreeSet 类 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。该类可以实现排序等功能。

3. 常用的 Map 图的实现子类

如下表:

类名 描述
HashMap 类 HashMap 类是一个散列表,它存储的内容是键-值对 (key-value) 映射。该类实现了 Map 接口,根据键的 HashCode 值存储元素,具有很快的访问速度,但最多允许一个元素的键为 null (空值),它不支持线程同步。
TreeMap 类 TreeMap 类继承了AbstractMap ,实现了大部分 Map 接口,并且使用一颗树。
HashTable 类 Hashtable 继承自 Dictionary(字典) 类,用来存储 键-值对。
Properties 类 Properties 继承自 HashTable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。

特此说明:由于集合框架的内容繁多,因此本文只介绍 Map 图 下的 Map 接口及其重要实现子类的内容,其余集合框架的知识将会在下篇博文分享。

二、Map 接口

1. Map 接口的特点

(1)Map 接口与 Collection 接口同级(彼此没有继承关系),Map 图存储一组 键-值 对象,提供 key(键)到 value(值)的映射。
(2)Map 图中的 key 和 value 可以是任何一种引用类型的数据,它们会被封装到 HashMap$Node 对象中。
(3)Map 图中的 key 不可以重复,但 value 可以重复;key 只允许存在一个 null (空值),value 可以存在多个 null(空值)。
(4)String 类常被用作 Map 中的 key。
(5)key 和 value 之间存在单向的一对一关系,即通过指定的 key 一定能找到对应的 value(因为 key 值是唯一的),反之不成立。

  • 代码演示:

public class Map_ {
    
    
    public static void main(String[] args) {
    
    
        // Map 接口实现类的特点, 使用实现类 HashMap

        Map map = new HashMap();// 创建一个图

        // 在 map 中,put 方法可以用来添加和替换元素
        map.put("no1", "韩顺平");// 成功
        map.put("no2", "张无忌");// 成功
        map.put("no1", "张三丰");// 添加失败,当有相同的 key , 原来的 value 就会被替换

        // put 方法还会返回被替换的 value,如果成功添加一个新元素,就会返回 null
        System.out.println(map.put("no1", "张三丰"));

        map.put("no3", "张三丰");// value 相同,但 key 不同,不会发生替换
        map.put(null, null); // 成功
        map.put(null, "abc"); // 添加失败,当有相同的 key , 原来的 value 就会被替换
        map.put("no4", null); // 成功
        map.put("no5", null); // 成功
        map.put(1, "赵敏");// 成功
        map.put(new Object(), "金毛狮王");// 成功

        // 通过get 方法,传入 key ,会返回对应的 value
        System.out.println(map.get("no2"));// 返回张无忌

        System.out.println("map=" + map);
    }
}

2. Map 图存放“键-值对”底层原理(重难点)

(1)在实现了 Map 接口的 HashMap 类中,定义了一个静态内部类 Node 类,Node 类中定义了属性 K key 和 V value ,因此,key-value 键值对实质上是存放在 HashMap 类中的 Node 类中的。
(2)静态内部类 Node 类同时又实现了 Map 接口中的局部接口 Entry 接口,因此,Node 类的对象(即结点)可以使用 Entry 接口中定义的方法了。

  • HashMap 类源码如下图:

在这里插入图片描述

(3)在创建一个 HashMap 类对象(哈希图)的同时,系统会自动创建一个 存放 Map.Entry 类型元素的 EntrySet 集合。
(4)当把一个 键值对 存放进 HashMap 中时,其实就是创建了一个HashMap$Node 对象(即结点);与此同时,在 EntrySet 集合中会自动创建一个 Map.Entry 类对象,该对象中存储着 HashMap$Node 对象的引用。

  • 如下图所示:

在这里插入图片描述

  • 代码演示说明:

public class MapSource_ {
    
    
    public static void main(String[] args) {
    
    

        Map map = new HashMap();// 向上转型

        map.put("no1", "韩顺平");
        map.put("no2", "张无忌");

/*  解读:

        1. k-v 实质存储位置: HashMap$Node node = newNode(hash, key, value, null)
        
        2. 为了方便程序员的遍历,系统会自动创建 EntrySet 集合 ,该集合存放的元素的类型为 Map.Entry, 而一个 Entry
           对象又指向一个 HashMap$Node。
        
        3. EntrySet 集合中, 存储对象的类型是 Map.Entry ,但实际上存放的还是 HashMap$Node 的引用。
           这是因为 static class Node<K,V> 类 实现了 Map.Entry<K,V> 接口。
        
        4. 当把 HashMap$Node 对象D的引用 存放到 EntrySet 集合后,就方便我们对 k-v 的遍历, 因为 Map.Entry接口中 提供了重要方法
           getKey()和 getValue() 用于对 k-v 的遍历。 

        5. Map 接口中提供了 keySet() 方法 和 values() 方法,分别用于返回一个只存储 key 的 Set 集合,和一个只存储 
            value 的 Collection 集合。
        */

        Set set = map.entrySet();
        System.out.println(set.getClass());// 运行类型为 HashMap$EntrySet

        for (Object obj : set) {
    
    

            System.out.println(obj.getClass()); // 运行类型为 HashMap$Node
            
            // 为了从 HashMap$Node 取出k-v,要 向下转型
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "-" + entry.getValue() );
        }

        // Map 接口提供的 keySet() 方法,返回一个只存储 key 的 Set 集合
        Set set1 = map.keySet();
        System.out.println(set1.getClass());


        // Map 接口提供的 values() 方法,返回一个只存储 value 的 Collection 集合。
        Collection values = map.values();
        System.out.println(values.getClass());

    }
}

3. Map 接口常用方法

  • 代码演示:

public class MapMethod {
    
    
    public static void main(String[] args) {
    
    

        Map map = new HashMap();

//      1.  put: 添加和替换,成功添加则返回 null,替换则返回被替换的 value
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");

        System.out.println("map=" + map);

//      2.  remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map=" + map);

//      3.  get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val=" + val);

//      4.  size:获取元素个数
        System.out.println("k-v=" + map.size());

//      5.  isEmpty:判断元素个数是否为0
        System.out.println(map.isEmpty());// F

//      6.  containsKey:查找键是否存在
        System.out.println("结果=" + map.containsKey("hsp"));// T

//      7.  clear:清除所有k-v
        map.clear();
        System.out.println("map=" + map);// []

    }
}

4. Map 接口遍历方法

Map 接口遍历方式:
方式一:先使用 Map 接口中的 keySet() 方法得到一个 只存储 key 的 Set 集合,再通过 key 调用 get() 方法,得到对应的 value。
方式二:直接使用 Map 接口中的 value() 方法得到一个 只存储 value 的 Collection 集合(不推荐使用该方法,因为得不到映射关系)。
方式三:先使用 Map 接口中的 entrySet() 方法,得到一个存储了 Entry 类对象的 EntrySet 集合,Entry 类对象指向了 key-value 的 HashMap$Node 结点,再调用 Entry 类对象 的getKey() 和 getVal() 方法,得到对应的 key 和 value。

  • 代码演示:

public class MapFor {
    
    
    public static void main(String[] args) {
    
    

        Map map = new HashMap();

        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");

        // 方式一:先使用 Map 接口中的 keySet() 方法得到一个 只存储 key 的 Set 集合,
        //        再通过 key 调用 get() 方法,得到对应的 value。
        Set keyset = map.keySet();

        // (1) 增强for
        System.out.println("-----第一种方式-------");
        for (Object key : keyset) {
    
    
            System.out.println(key + "-" + map.get(key));
        }
        
        // (2) 迭代器
        System.out.println("----第二种方式--------");
        Iterator iterator = keyset.iterator();
        while (iterator.hasNext()) {
    
    
            Object key =  iterator.next();
            System.out.println(key + "-" + map.get(key));
        }

        // 方式二:直接使用 Map 接口中的 value() 方法得到一个只存储 value 的 Collection 集合
        //      (不推荐使用该方法,因为得不到映射关系)。
        
        Collection values = map.values();
        // 这里可以使用所有的Collections使用的遍历方法
         
        // (1) 增强for
        System.out.println("---取出所有的value 增强for----");
        for (Object value : values) {
    
    
            System.out.println(value);
        }
        // (2) 迭代器
        System.out.println("---取出所有的value 迭代器----");
        Iterator iterator2 = values.iterator();
        while (iterator2.hasNext()) {
    
    
            Object value =  iterator2.next();
            System.out.println(value);

        }

        //方式三:先使用 Map 接口中的 entrySet() 方法,得到一个存储了 Entry 类对象的 EntrySet 集合,
        //      Entry 类对象指向了 key-value 的 HashMap$Node 结点,
        //      再调用 Entry 类对象 的getKey() 和 getVal() 方法,得到对应的 key 和 value。
        Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>
        
        // (1) 增强for
        System.out.println("----使用EntrySet 的 for增强(第3种)----");
        for (Object entry : entrySet) {
    
    
            //将entry 转成 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }

        // (2) 迭代器
        System.out.println("----使用EntrySet 的 迭代器(第4种)----");
        Iterator iterator3 = entrySet.iterator();
        while (iterator3.hasNext()) {
    
    
            Object entry =  iterator3.next();
            // System.out.println(next.getClass());// HashMap$Node 类 -实现-> Map.Entry接口 (getKey, getValue)
            
            // 向下转型 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
}

三、HashMap 类(哈希图)

1. HashMap 类的特点

(1)HashMap 类也是一个散列表,该类实现了 Map 接口,它存储的内容是键-值对 (key-value) 映射。它具有很快的访问速度,但不支持线程同步,是不安全的。
(2)HashMap 中的 key 不可以重复,但 value 可以重复;key 只允许存在一个 null (空值),但 value 可以存在多个 null(空值)。
(3)HashMap 类与 HashSet 类一样,存储的元素是无序的(因为它是根据键的 HashCode 值存储元素的),它们的底层都维护了 数组 + 链表 + 红黑树 的数据结构

2. HashMap 底层机制及源码剖析(重难点 )

2.1 HashMap 添加 key-value 的底层机制

  • 源码分析:

(1)HashMap 底层维护了 Node 类型的数组 table , 默认容量为 nul。
(2)当创建 HashMap 类的对象时,将加载因子 (loadfactor) 初始化为 0.75。
(3)当添加 key-value 时,通过 key 的哈希值得到在 table 的索引,然后判断该索引位置的结点是否为 null 。
(4)如果为 null 则直接添加 key-value;如果该结点不为 null,继续判断该结点的 key 和准备加入的 key 是否相等;如果相等, 则直接替换两者的 value ; 如果不相等则需要判断是索引连接的结点是树结构还是链表结构。
(5)如果是树结构,就采用树结构的树结构中的方法,此处不深入探讨。
(6)如果是链表结构,就遍历链表中每个结点,如果不存在结点的 key 值与新结点的 key 相等,则将新结点添加在链表尾部,并判断是否要树化;如果存在结点的 key 值 与新结点的 key 值相等,则直接替换两者对应的 value 。

  • 底层数据结构示意图:

在这里插入图片描述

  • 源码剖析:

public class HashMapSource1 {
    
    
    public static void main(String[] args) {
    
    
        HashMap map = new HashMap();
        map.put("java", 10);// ok
        map.put("php", 10);// ok
        map.put("java", 20);// 替换value

        System.out.println("map=" + map);//

源码剖析:

1. 执行构造器 new HashMap()

   初始化加载因子 loadfactor = 0.75
   HashMap$Node[] table = null

2. 执行put()方法, 先调用 hash 方法,计算 key 的 hash值 (h = key.hashCode()) ^ (h >>> 16)

    public V put(K key, V value) {
    
    //K = "java" value = 10
        return putVal(hash(key), key, value, false, true);
    }

3. 执行 putVal()方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
           boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;// 辅助变量

        // 如果底层的 table 数组为 null, 或者 length =0 , 就调用 resize()方法 进行扩容到 16 (第一次扩容)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把准备加入的 k-v
        //  封装成一个 Node ,加入该位置即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        // 如果该索引位置的 Node 不为 null,则进入该分支
        else {
    
    

            Node<K,V> e; K k;// 辅助变量
         // 如果 table 的索引位置的 key 的 hash 和新元素的 key 的 hash 值相同,
         // 并且满足 (table 现有的结点的 key 和准备添加的 key 是同一个对象  || equals返回真)
         // 则说明: 新结点和数组中旧结点的 key 相同,
         // 就将辅助结点 e 指向 p,并直接替换该结点原有的 value值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

        //  如果 新结点和数组中旧结点的 key 不相同,且 数组连接的是 红黑树,则进入该分支,不细讲
            else if (p instanceof TreeNode)// 如果当前的table的已有的 Node 是红黑树,就按照红黑树的方式处理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        //  如果 新结点和数组中旧结点的 key 不相同, 则进入数组连接的链表,
        //  循环遍历比较链表中每个结点的 key 与 新结点的 key 是否相同。       
            else {
    
    
                
                for (int binCount = 0; ; ++binCount) {
    
    // 死循环

                    // 首先,辅助结点 e 直接指向 p 的下一个结点,
                    // 然后判断该结点是否存在 元素,如果不存在,说明 新结点 要添加进链表中
                    // 且此时的 e == null,不会执行最下面交换 value 的代码
                    if ((e = p.next) == null) {
    
    
                        p.next = newNode(hash, key, value, null);

                        // 加入后,立刻判断当前链表的结点个数,是否已经到8个,到8个后
                        // 就调用 treeifyBin() 方法对链表进行红黑树的转换
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;// 然后直接退出循环
                    }

                    //  如果在循环比较过程中,发现旧结点有相同的 key, 就直接 break,
                    //  此时的 e 不为 null,所以会进入下面交换 value 的代码
                    if (e.hash == hash && 
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

                    // 如果前面的两个判断条件都不满足,说明要继续向下一个结点进行比较
                    // 这里更新辅助结点 p 的指向,然后进入下次循环的起始位置,辅助结点 e 的指向也会更新 
                    p = e;
                }
            }

            // 这部分代码负责交换 value,只要进入该代码,都会交换两结点的value
            // 只要 e 指向的结点不为空,就代表存在旧结点与新结点的 key 相同
            // 无论它们的 value 相不相同,都交换它们的 value。
            if (e != null) {
    
     
                V oldValue = e.value;

                // !onlyIfAbsent 始终为真,是传入的参数
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value; //替换 两结点 key 对应的 value
                afterNodeAccess(e);

                // 退出 putVal()方法,返回被替换的 value,注意size和modcount没有增加。 
                return oldValue;
            }
        }

        ++modCount;// 每增加一个 Node 结点 ,就 size++,modCount++
        if (++size > threshold)// 如果size > 临界值,就进行数组扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

4. 补充:关于树化的过程(链表转成红黑树)
    // 进入 putTreeVal()方法后,不会马上树化,而是先判断数组 table 的长度
    // 如果 table 为 null ,或者其大小(size)还没有到 64,暂时不树化,而是进行数组扩容.
    // 直到满足 size > 64 ,且 某条链表上结点的个数 >= 8 这两个条件
    // 才会真正的树化 -> 剪枝。

    final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
    }
         
    }
}

2.2 HashMap 底层扩容机制

(1)HashMap 使用 put() 方法添加 k-v 时,会调用 resize() 方法进行数组的扩容。
(2)当第一次添加 k-v 时,resize()方法会将数组的大小 (size 变量)扩容到16;并在方法中设置了一个数组临界值 threshold ,是数组大小的 0.75 倍,当数组中存储的元素到达该临界值的时候,就会再次调用 resize()方法对数组进行扩容,默认是扩容到之前数组大小的2倍,然后更新临界值 threshold。
(3)当 HashTable 中的某个数组位置中的链表的结点个数到达一个固定值时(默认为 8个),哈希表就会将普通的单向链表进行红黑树化,但前提是数组的大小已到达64;否则要先对数组扩容(直到数组大小到达64),再进行树化。

  • 源码剖析:

public class HashMapIncrement {
    
    
    public static void main(String[] args) {
    
    
        /*
        HashMap 第一次添加时 k-v 时,table 数组扩容到 16,
        临界值(threshold)是 16 * 加载因子(loadFactor == 0.75) = 12
        如果table 数组使用到了临界值 12, 就会将数组扩容到 16 * 2 = 32,
        新的临界值就是 32 * 0.75 = 24, 依次类推
         */
        HashMap map = new HashMap();

        // 往哈希图中添加 k-v ,下列的每个 k-v 都是添加到集合的数组中,不会添加到链表中
        // 所以哈希图将会一直进行数组的扩容。
        for(int i = 1; i <= 100; i++) {
    
    
            map。put("" + i, i); 
        }

        /*
        在Java8中, 如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是 8 ),
        并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
        否则仍然采用数组扩容机制,意思是说到第11个元素时就会扩容到64了
         */
        map = new HashMap(); // 创建一个新的哈希图

        // 往哈希图中添加 key ,但与上面不同,这次添加的 k-v 会添加到数组的同一个索引位置,
        // 因此数组的大小不会改变,依旧是默认的 16。
        // 由于每个结点的 key 都不同,所以这些结点会加入到数组的单向链表中,
        // 当链表中的结点增加到8个时,哈希图就要将普通链表进行树化;
        // 但此时数组的大小为 16,不满足树化要求的数组大小为 64,所以要先进行数组的扩容;
        // 则新加入的第9、10个结点依然是添加到单向链表的后面,此时数组大小扩容到 64;
        // 在添加第11 个结点时,数组大小和链表长度都满足了树化的条件,因此哈希图将链表进行树化。
        // 注意,本例中的每个结点都是不同的,但他们的 hash 值相同,因此加入的数组的索引位置相同。
        for(int i = 1; i <= 12; i++) {
    
    
            map.put(new A(i), i);
        }


        /*
            当我们向 map 增加一个元素,-> Node -> 加入table , 就算是增加了一个size
            在 table中 size > threshold ,就会扩容
         */

        map = new HashMap(); // 再次创建一个新的哈希图

        // 在 哈希图的某一条链表上添加了 7个 A对象-100 键值对
        for(int i = 1; i <= 7; i++) {
    
    
            map.put(new A(i), 100);
        }
        
        // 在另一条链表上添加到第4 个 B对象-200 键值对的时候,size = 12,到达临界值,数组会进行 resize()扩容
        // 但是由于为满足某条链表的结点个数 >= 8,所以不会进行树化。 
        for(int i = 1; i <= 7; i++) {
    
    
            map.put(new B(i), 200);
        }
    }
}

class B {
    
    
    private int n;

    public B(int n) {
    
    
        this.n = n;
    }
    @Override
    public int hashCode() {
    
    
        return 200;
    }
}

class A {
    
    
    private int n;

    public A(int n) {
    
    
        this.n = n;
    }
    @Override
    public int hashCode() {
    
    
        return 100;
    }
}
  • 以上就是 HashMap 在添加 key-value 时底层扩容的实现过程,小伙伴们要自己去 debug 一遍,才能深入理解。

四、HashTable 类(哈希表)

HashTable类的特点

(1)HashTable 也是一个散列表,它存储的内容是键值对(key - value)的映射。
(2)HashTable 继承于 Dictionary 类,实现了Map 接口。
(3)HashTable 是可同步的,这意味着它是线程安全的。
(4)HashTable 的 key 和 value都不可以为 nul,否则会抛出 NullPointerException 异常。
(5)HashTable 的使用方法和 HashMap 的基本一样,可以使用 Map 接口中的所有方法。
(6)底层有数组 Hashtable$Entry[] ,数组默认初始化大小为 11,临界值 threshold = 11 * 0.75 = 8;默认的扩容机制为原先的 2倍 + 1。

  • 代码演示:

public class HashTableExercise {
    
    
    public static void main(String[] args) {
    
    

        Hashtable table = new Hashtable();

        table.put("john", 100); // ok
        // table.put(null, 100); // 添加失败,抛出异常 NullPointerException
        // table.put("john", null);// 添加失败,抛出异常 NullPointerException
        table.put("lucy", 100);// ok
        table.put("lic", 100);// ok
        table.put("lic", 88);// 100 被替换成 88

        System.out.println(table);
    }
}

五、Properties 类

Properties 类的基本介绍

在这里插入图片描述

  • 代码演示:

public class Properties_ {
    
    
    public static void main(String[] args) {
    
    

        //1. Properties 继承  Hashtable
        //2. 可以通过 k-v 存放数据,当然key 和 value 不能为 null
        //增加
        Properties properties = new Properties();
        //properties.put(null, "abc");// 抛出 空指针异常
        //properties.put("abc", null); // 抛出 空指针异常
        properties.put("john", 100);
        properties.put("lucy", 100);
        properties.put("lic", 100);
        properties.put("lic", 88);// 如果有相同的key , value被替换

        System.out.println("properties=" + properties);

        // 通过k 获取对应值
        System.out.println(properties.get("lic"));// 88

        // 删除
        properties.remove("lic");
        System.out.println("properties=" + properties);

        // 修改
        properties.put("john", "约翰");
        System.out.println("properties=" + properties);

    }
}

六、TreeMap 类(树图)

  • 源码分析:

public class TreeMap_ {
    
    
    public static void main(String[] args) {
    
    

        // 使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)

        // TreeMap treeMap = new TreeMap();
    
        // 使用构造器创建
        TreeMap treeMap = new TreeMap(new Comparator() {
    
    
            @Override
            public int compare(Object o1, Object o2) {
    
    
                
             // return ((String) o2).compareTo((String) o1);  按照传入的 k(String) 的大小进行排序
                
                return ((String) o2).length() - ((String) o1).length();// 按照K(String) 的长度大小排序
            }
        });

        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("kristina", "克瑞斯提诺");
        treeMap.put("smith", "斯密斯");
        treeMap.put("hsp", "韩顺平");// 加入不了,因为hsp 和 tom长度相同

        System.out.println("treemap=" + treeMap);


源码分析:

1. 构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给 TreeMap 的 comparator

    public TreeMap(Comparator<? super K> comparator) {
    
    
        this.comparator = comparator;
    }

2. 调用 put()方法

    2.1 第一次添加 k- v, 把 k-v 封装到 Entry对象,放入 root

    Entry<K,V> t = root;
    if (t == null) {
    
    
        compare(key, key); 

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }

    2.2 以后再添加 k- v

    Comparator<? super K> cpr = comparator;

    if (cpr != null) {
    
    

        // 遍历所有的key , 给当前 key 找到适当位置
        do {
    
     
            parent = t;
            cmp = cpr.compare(key, t.key);// 动态绑定到我们的匿名内部类的 compare方法
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;

            else  // 如果遍历过程中,发现准备添加 Key 和当前已有的 Key 相等,就不添加
                return t.setValue(value);
        } while (t != null);
    }

    }
}

七、Collections 工具类

(1)Collections 是一个操作 Set、List 和 Map 等 集合/图 的工具类。
(2)Collections 中提供了一系列静态的方法对 集合/图 中的元素进行排序、查询、修改等操作。

  • 方法介绍:
    在这里插入图片描述
    在这里插入图片描述

  • 代码演示:


public class Collections_ {
    
    
    public static void main(String[] args) {
    
    

        //创建ArrayList 集合,用于测试
        
        List list = new ArrayList();
        list.add("tom");
        list.add("smith");
        list.add("king");
        list.add("milan");
        list.add("tom");


//      1.  reverse(List):反转 List 中元素的顺序
        Collections.reverse(list);
        System.out.println("list=" + list);

//      2.  shuffle(List):对 List 集合元素进行随机排序
       for (int i = 0; i < 5; i++) {
    
    
           Collections.shuffle(list);
           System.out.println("list=" + list);
       }

//      3.  sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
        Collections.sort(list);
        System.out.println("自然排序后");
        System.out.println("list=" + list);

//      4.  sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
      
        // 按照 字符串的长度大小排序
        Collections.sort(list, new Comparator() {
    
    
            @Override
            public int compare(Object o1, Object o2) {
    
    
                //可以加入校验代码.
                return ((String) o2).length() - ((String) o1).length();
            }
        });
        System.out.println("字符串长度大小排序=" + list);


//      5.  swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

        Collections.swap(list, 0, 1);
        System.out.println("交换后的情况");
        System.out.println("list=" + list);

//      6.  Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
        System.out.println("自然顺序最大元素=" + Collections.max(list));

//      7.  Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
      
        //比如,我们要返回长度最大的元素
        Object maxObject = Collections.max(list, new Comparator() {
    
    
            @Override
            public int compare(Object o1, Object o2) {
    
    
                return ((String)o1).length() - ((String)o2).length();
            }
        });
        System.out.println("长度最大的元素=" + maxObject);


//      8.  Object min(Collection)
//      9.  Object min(Collection,Comparator)
        //上面的两个方法,参考max即可

//      10. int frequency(Collection,Object):返回指定集合中指定元素的出现次数
        System.out.println("tom出现的次数=" + Collections.frequency(list, "tom"));

//      11. void copy(List dest,List src):将src中的内容复制到dest中

        ArrayList dest = new ArrayList();
        // 为了完成一个完整拷贝,我们需要先给dest 赋值,大小和list.size()一样
        for(int i = 0; i < list.size(); i++) {
    
    
            dest.add("");
        }
        // 拷贝
        Collections.copy(dest, list);
        System.out.println("dest=" + dest);

//      12. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

        // 如果list中,有tom 就替换成 汤姆
        Collections.replaceAll(list, "tom", "汤姆");
        System.out.println("list替换后=" + list);

    }
}

总结

开发中如何选择集合实现类(记住)

在这里插入图片描述

感受

  • 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
  • 本文详细介绍了 集合框架 的基本概念,并深入讲解了 Map 图中常用的 Map 接口、HashMap 类、HashTable 类、TreeMap 类 使用的注意事项和常用方法;还分析了各个子类实现的源码,举了很多很多例子,希望小伙伴们看后能有所收获!
  • 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!

猜你喜欢

转载自blog.csdn.net/weixin_45395059/article/details/125850642