集合

4-Map接口

2021-07-13 615 0

简介 本文介绍Map接口体系,深入JKD解析HashMap源码,介绍了Map常用的方法。

1. Map接口体系

upfile


|----Map: since JDK1.2  双列数据,存储key-value对的数据

        |----HashMap: since JDK1.2 作为Map的主要实现类;线程不安全的,效率高;可存储nullkeyvalue

             |----子类 LinkedHashMap: since JDK1.4, 保证在遍历map元素时,可以按照添加的顺序实现遍历。

                     原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。

                     对于频繁的遍历操作,此类执行效率高于HashMap

        |----TreeMap: since JDK1.2 保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序

                     底层使用红黑树

        |----Hashtable: since JDK1.0 作为古老的实现类;线程安全的,效率低;不能存储nullkeyvalue

             |----子类 Properties:常用来处理配置文件。keyvalue都是String类型

     HashMap的底层:数组+链表   (jdk7及之前)

                                   数组+链表+红黑树  (jdk 8


Map结构的理解:

   Map中的key:无序的、不可重复的,使用Set存储所有的key  ---> key所在的类要重写equals()和hashCode() (以HashMap为例)

   Map中的value:无序的、可重复的,使用Collection存储所有的value --->value所在的类要重写equals() containsValue方法会使用equals

   一个键值对:key-value构成了一个Entry对象。

   Map中的entry:无序的、不可重复的,使用Set存储所有的entry

 Map与Collection并列存在。用于保存具有映射关系的数据:key-value

 Map 中的 key 和 value 都可以是任何引用类型的数据

 Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法

 常用String类作为Map的“键”

 key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value

 Map接口的常用实现类: HashMap、 TreeMap、 LinkedHashMap和Properties。 其中, HashMap是 Map 接口使用频率最高的实现类


2.  JDK7 HashMap

HashMap的底层实现原理?以jdk7为例说明:

     HashMap map = new HashMap():

     在实例化以后,底层创建了长度是16的一维数组 Entry[] table。

     ...多次put之后...

     map.put(key1,value1):

     首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值(比较大)经过某种算法(哈希函数、散列函数)计算以后,得到在Entry数组中的存放位置。

     如果此位置上的数据为空,此时的key1-value1(Entry)添加成功。 ----情况1

     如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

             如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1(Entry)添加成功。----情况2

             如果key1的哈希值和已经存在的某一个数据(key2-value2,Entry)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较(不同的对象可能存在hash值相同的情况)

                     如果equals()返回false:此时key1-value1添加成功。----情况3

                     如果equals()返回true:使用value1替换value2。

      补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。


部分源码分析:

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

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    // 临界值 = 容量 * 加载因子, 初始状态是 16 * 0.75
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始状态长度为16的 Entry数组
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    // 获取 key 对象的 hash值
    int hash = hash(key);
    // 通过hash值,获取该key在数组中的下标
    int i = indexFor(hash, table.length);
    // i位置有数据
    for (Entrye = table[i]; e != null; e = e.next) {
        Object k;
        // 先判断 hash 值是否相等,如果所有的元素跟要添加的元素,hash值不相等,那么跳出循环,添加元素
        // 如果hash值相等,那么判断地址是否一样,然后判断两个对象是否equals
        // 如果地址都一样,那么进入if,替换原有key对应的的value
        // 如果地址不一样,但是equals返回true,说明两个对象相等, 那么也替换原有key对应的value
        // 如果所有的hash值相等,但是equals都不相等,那么就需要添加这个 Entry
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // i位置没有数据, 那么直接可以添加 Entry 元素
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
       // 如果要添加元素的位置为空,那么可以添加
       // 如果大于等于临界值,并且要添加的位置不为空,那么就扩容为原来的2倍
       if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    // 不需要扩容那么 重新创建 Entry
    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entrye = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}


3.  JDK 8 HashMap

jdk8 相较于jdk7在底层实现方面的不同:

     1. new HashMap():底层没有创建一个长度为16的数组

     2. jdk 8底层的数组是:Node[],而非 Entry[],其实内容一样

     3. 首次调用put()方法时,底层创建长度为16的数组

     4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。

        4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)

         4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

   

     HashMap map = new HashMap():

     在实例化以后,底层只是初始化了一个加载因子 this.loadFactor = DEFAULT_LOAD_FACTOR; ,该值为0.75,

     首次map.put(key1,value1):底层初始化一个长度为 16 的 Node数组

多次put之后...

     首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值(比较大)经过某种算法(哈希函数、散列函数)计算以后,得到在Node数组中的存放位置(也就是常说的hash bucket)。

     如果此位置上的数据为空,此时的key1-value1(Node)添加成功(需要判断是否满足转换为红黑树的条件)

     如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表或树形式存在)),比较key1和已经存在的一个或多个数据

     的哈希值:

             如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1(Entry)添加成功。(需要判断是否满足转换为红黑树的条件)

             如果key1的哈希值和已经存在的某一个数据(key2-value2,Entry)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:(不同的对象可能存在hash值相同的情况)

                     如果equals()返回false:此时key1-value1添加成功

                     如果equals()返回true:使用value1替换value2

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量,16

     DEFAULT_LOAD_FACTOR:HashMap 的默认加载因子:0.75,兼顾了数组的利用率,也兼顾了链表不能太长

     threshold:扩容的临界值,= 容量 * 填充因子:16 * 0.75 => 12

     TREEIFY_THRESHOLD: Bucket 中链表长度大于该默认值,转化为红黑树: 8

     MIN_TREEIFY_CAPACITY:桶中的 Node 被树化时最小的 hash 表容量 : 64


部分源码分析:

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// hash(key) 获取hash值
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Nodep; int n, i;
    // 首次的时候 数组的大小为16,临界值为 12
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    // i 要存放的数组的索引位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果是null, 添加元素成功
        tab[i] = newNode(hash, key, value, null);
    else {
        Nodee; K k;
        // 如果 hash值相等,或者是同一个对象,或者equals返回true,那么即将添加的元素已有, 走下面的替换逻辑
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 是一个 红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        // 是链表
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 链表长度大于8 的时候,可能需要转换为红黑树, treeifyBin中还会判断是否转换为红黑树 treeifyBin
                    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;
}


final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    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
        // 首次分别是 16 和 12 , 加载因子 0.75 
        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[] newTab = (Node[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Nodee;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    NodeloHead = null, loTail = null;
                    NodehiHead = null, hiTail = null;
                    Nodenext;
                    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);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}


final void treeifyBin(Node[] tab, int hash) {
    int n, index; Nodee;
    // 如果数组的长度为 小于 64,数组扩容, 大于等于 64,转换为红黑树
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNodehd = null, tl = null;
        do {
            TreeNodep = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}


4. LinkedHashMap的底层实现

能够记录元素添加的先后顺序,遍历的时候,按顺序遍历

在HashMap基础上增加了 before 和 after 的引用

static class Entryextends HashMap.Node{
    Entrybefore, after;
    Entry(int hash, K key, V value, Nodenext) {
        super(hash, key, value, next);
    }
}


5.CurrentHashMap 分段锁


6. Map中常用的方法

总结:常用方法:

添加:put(Object key,Object value)

删除:remove(Object key)

修改:put(Object key,Object value)

查询:get(Object key)

长度:size()

遍历:keySet() / values() / entrySet()

6.1 添加、删除、修改操作

Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中

void putAll(Map m):将m中的所有key-value对存放到当前map中

Object remove(Object key):移除指定key的key-value对,并返回value

void clear():清空当前map中的所有数据

//MapMethodTest.java
package com.ylaihui.mapitf;

import java.util.HashMap;

public class MapMethodTest {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("111","aaa");
        map.put("222","bbb");
        map.put("333","ccc");
        map.put("444","ddd");
        map.put("222","eee");

        System.out.println(map);  // {111=aaa, 222=eee, 333=ccc, 444=ddd}

        HashMap map1 = new HashMap();
        map1.put("111","abc");
        map1.put("222","abc");

        map.putAll(map1);
        System.out.println(map);  // {111=abc, 222=abc, 333=ccc, 444=ddd}

        map.remove("111");
        System.out.println(map);  // {222=abc, 333=ccc, 444=ddd}

        map.clear();
        System.out.println(map.size()); // 0
    }
}


6.2 元素查询的操作

Object get(Object key):获取指定key对应的value

boolean containsKey(Object key):是否包含指定的key

boolean containsValue(Object value):是否包含指定的value

int size():返回map中key-value对的个数

boolean isEmpty():判断当前map是否为空

boolean equals(Object obj):判断当前map和参数对象obj是否相等

//MapMethodTest2.java
package com.ylaihui.mapitf;

import java.util.HashMap;

public class MapMethodTest2 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("111","aaa");
        map.put("222","bbb");
        map.put("333","ccc");
        map.put("444","ddd");

        Object obj = map.get("333");
        System.out.println(obj);  // ccc

        boolean b = map.containsKey("111");
        System.out.println(b);  // true

        b = map.containsValue("ccc");
        System.out.println(b);  // true

        System.out.println(map.size()); // 4

        System.out.println(map.isEmpty());  // false

        HashMap map1 = new HashMap();
        map1.put("333","ccc");
        map1.put("111","aaa");
        map1.put("222","bbb");
        map1.put("444","ddd");

        System.out.println(map.equals(map1)); // true
    }
}

6.3 Map的遍历操作

Set keySet():返回所有key构成的Set集合

Collection values():返回所有value构成的Collection集合

Set entrySet():返回所有key-value对构成的Set集合

//MapMethodTest3.java
package com.ylaihui.mapitf;

import java.util.*;

public class MapMethodTest3 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("111","aaa");
        map.put("222","bbb");
        map.put("333","ccc");

        // 通过 Set 集合遍历 所有的key, 通过key获取 value
        Set set = map.keySet();
        Iterator it = set.iterator();
        while(it.hasNext()){
            Object next = it.next();
            Object value = map.get(next);
            System.out.println("key:" + next + ", value:" + value);
        }

        // 遍历所有的value
        Collection values = map.values();
        Iterator iterator = values.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }

        // 遍历所有的 Entry,通过Entry的getKey 和 getValue获取 k-v
        Set set1 = map.entrySet();
        Iterator it1 = set1.iterator();
        while (it1.hasNext()){
            Object next = it1.next();
            //System.out.println(next.getClass());
            Map.Entry next1 = (Map.Entry) next;
            System.out.println(next1.getKey() + ":" + next1.getValue());
        }
    }
}
/*
key:111, value:aaa
key:222, value:bbb
key:333, value:ccc
aaa
bbb
ccc
111:aaa
222:bbb
333:ccc
 */


7. TreeMap的使用-自然排序

涉及到排序

可自然排序和定制排序

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象

因为要按照key进行排序:自然排序 、定制排序

注意: 不能按照value排序

//LinkedHashMapTest.java
package com.ylaihui.mapitf;

import java.util.*;

class Order implements Comparable{
    int id;
    Date date;

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", date=" + date +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return id == order.id &&
                Objects.equals(date, order.date);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, date);
    }

    public Order(int id, Date date) {
        this.id = id;
        this.date = date;
    }

    public Order() {
    }

    @Override
    public int compareTo(Object o) {
        if(o instanceof Order){
            Order order = (Order) o;
            return this.id - order.id;
        }else
            throw new RuntimeException("不能比较的类型");
    }

}

public class LinkedHashMapTest {
    public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap();

        Order o1 = new Order(111, new Date());
        Order o2 = new Order(222, new Date());
        Order o3 = new Order(333, new Date());
        Order o4 = new Order(444, new Date());

        map.put(o1,"o1");
        map.put(o2,"o2");
        map.put(o3,"o3");
        map.put(o4,"o4");

        Set set = map.entrySet();
        Iterator it = set.iterator();
        while (it.hasNext()){
            Object next = it.next();
            Map.Entry entry = (Map.Entry) next;
            System.out.println( entry.getKey() +":"+entry.getValue());
        }

    }
}


8.TreeMap的使用-定制排序

//TreeMapTest1.java
package com.ylaihui.mapitf;

import java.util.*;

class Book{
    int id;
    String name;

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return id == book.id &&
                Objects.equals(name, book.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

    public Book(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public Book() {
    }
}
public class TreeMapTest1 {
    public static void main(String[] args) {
        TreeMap map = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Book && o2 instanceof Book){
                    Book b1 = (Book) o1;
                    Book b2 = (Book) o2;
                    return -Integer.compare(b1.id, b2.id);
                }
                else
                    throw new RuntimeException("不能比较的类型");
            }
        });

        Book b1 = new Book(111, "ylaihui");
        Book b2 = new Book(222, "ylaihui");
        Book b3 = new Book(555, "ylaihui");
        Book b4 = new Book(444, "ylaihui");

        map.put(b1,"ylaihui1");
        map.put(b2,"ylaihui2");
        map.put(b3,"ylaihui3");
        map.put(b4,"ylaihui4");

        Set set = map.entrySet();
        Iterator it = set.iterator();
        while (it.hasNext()){
            Object next = it.next();
            Map.Entry entry = (Map.Entry) next;
            System.out.println(entry.getKey() +":"+entry.getValue());
        }
    }
}
/*
Book{id=555, name='ylaihui'}:ylaihui3
Book{id=444, name='ylaihui'}:ylaihui4
Book{id=222, name='ylaihui'}:ylaihui2
Book{id=111, name='ylaihui'}:ylaihui1
 */


9. Hashtable

 Hashtable是个古老的 Map 实现类, JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。

 Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。

 与HashMap不同, Hashtable 不允许使用 null 作为 key 和 value

 与HashMap一样, Hashtable 也不能保证其中 Key-Value 对的顺序

 Hashtable判断两个key相等、两个value相等的标准, 与HashMap一致。

10. Properties的使用

Properties 类是 Hashtable 的子类,该对象用于处理属性文件

由于属性文件里的 key、 value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型

存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

Properties:常用来处理配置文件。key和value都是String类型


jdbc.properties

name=root 

password=12345678

//PropertiesTest.java
package com.ylaihui.mapitf;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args) {
        Properties pr = new Properties();

        try {
            pr.load(new FileInputStream("jdbc.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }

        System.out.println(pr.getProperty("name"));
        System.out.println(pr.getProperty("password"));
    }
}


点赞 0

文章评论

欢迎您:

纸上得来终觉浅,绝知此事要躬行!

112 文章 59532 浏览 3 评论

联系我

  •   QQ:    361352119
  •  Email:  lisimmy@sina.com
  • 微信: