Java集合之Map接口

本文详细介绍了Java中的Map接口及其常见实现类HashMap、HashTable和TreeMap。HashMap是基于数组+链表+红黑树的数据结构,允许key和value为null,而HashTable不支持null值,保证线程安全。TreeMap则是有序的,可以根据指定规则排序。文章还探讨了它们的存储结构、扩容机制以及添加元素的方法。
摘要由CSDN通过智能技术生成

1. Map

  1. 存储有映射关系的数据key-value
  2. key不允许重复,value可以重复,相同的key值,后加入的value会替换掉之前的value;
  3. key和value都可以为null,但key只能有一个null。

1.1 Map接口常用方法

  • V put(K key, V value) 添加k-v,返回添加value值
  • V remove(Object key):根据key删除,返回删除value值
  • boolean isEmpty():判断是否为空
  • void clear():清空map
  • boolean containsKey(Object key):判断是否存在key
  • Set<Map.Entry<K,V>> entrySet():返回指向键值对的entrySet
  • Set<K> keySet():返回存放key集合的set
  • Collection<V> values():返回存放value的集合。

1.2 HashMap

Java7及之前的HashMap的底层结构是数组+链表,在Java8及之后底层由数组+链表+红黑树组成。

1.2.1 存储结构

  • Map中存放的键值对都是放在一个实现类了Map.Entry接口的Node对象中:
	// 静态内部类,存放 k-v 键值对
    static class Node<K,V> implements Map.Entry<K,V> {
        // 根据key计算出的hash
        final int hash;
        final K key;
        V value;
        // 指向下一个节点
        HashMap.Node<K, V> next;

        Node(int hash, K key, V value, HashMap.Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
  • 并且为了方便遍历在HashMap存在一个EntrySet集合,看似存放的是Map.Entry<K,V>实际上 “存放” 的是向上转型之后的HashMap.Node<K,V>(EntrySet中没有创建新的Entry对象,而是指向HashMap中创建的Node对象),常用方法getKey()getValue()都是由这个Entry提供的:
transient Set<Map.Entry<K,V>> entrySet;

部分方法:

	interface Entry<K,V> {
 		K getKey();
       
        V getValue();
    }

1.2.1 存储和扩容机制

因为HashSet底层是使用HashMap实现的,所以这部分可以参考上期博客。

  • 构造函数:初始化加载因子为0.75
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • put请参考上篇博客。
  • 扩容和树化触发:

树化方法(部分):只有table的长度超过64时才会进行树化,否则进行扩容。

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

扩容方法:如果在某一索引后面链接的节点超过8个就会对table进行扩容(默认扩大两倍)

测试代码(Hash值相同):因为修改了所用作为key的A对象的hash值相等,也就意味着除第一个存在table表中,其它的都会以链表形式链接在后面,等到链接数量超过8个就会对table进行扩容,知道table的长度超过64时会进行树化。

/**
 * @Description HashMap 树化 和 扩容
 * @date 2022/4/4 9:01
 */
public class HashMapToTree {
    public static void main(String[] args) {
        HashMap<A,String > map = new HashMap<>();
        for (int i = 0; i < 12; i++) {
            // 对象的hashCode 相同, 但是equals不同,所以这里也有12条数据
            map.put(new A(i),"Hello Map");
        }

        System.out.println(map);
    }
}
class A{
    private int num;

    public A(int num){
        this.num = num;
    }

    // 重写当前对象的hashCode,使其全部一样
    // hash值相同,新加入对象就不会占用table空间,是以链表的方式添加到后面
    @Override
    public int hashCode() {
        return 100;
    }

    @Override
    public String toString() {
        return "A{" +
                "num=" + num +
                '}';
    }
}

测试代码(Hash值不同):如果hash值不同的情况下,会将这个对象放在不同的索引位置,直到 table 长度超过临界值 ,会对table进行扩容。

public static void diffHash(){
        HashMap<Integer ,String > map = new HashMap<>();
        // 因为不能保证hash值一定不同,多运行几次。
        for (int i = 0; i < 20; i++) {
            map.put((int) (Math.random() * 100),"A");
        }
    }

在这里插入图片描述

1.2 HashTable

  1. 不允许在键和值上放null,否则抛出空指针异常;
  2. 使用synchronized来保证线程安全。

1.2.1 扩容机制

新创建的HahsTable
**加粗样式**
当长度超过临界值时进行扩容之后:
在这里插入图片描述

  • 添加方法:显然HashTable是一个头插法的单向链表+数组实现的。
	/**
     * 添加方法
     * @param key k
     * @param value v
     * @return v
     */
    public synchronized V put(K key, V value) {
        // 值为空抛出异常
        if (value == null) {
            throw new NullPointerException();
        }

        Hashtable.Entry<?,?> tab[] = table;
        // 获取hashCode
        int hash = key.hashCode();
        // 根据hashCode计算索引
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        // 获取当前索引位置的对象
        Hashtable.Entry<K,V> entry = (Hashtable.Entry<K,V>)tab[index];
        // 判断当前索引位置是否有对象
        // 如果有对象,就用next获取它的链表对象
        for(; entry != null ; entry = entry.next) {
            // 如果计算出的hash值相同,并且key相同
            if ((entry.hash == hash) && entry.key.equals(key)) {
                // 新值替换旧值
                V old = entry.value;
                entry.value = value;
                // 返回旧值
                return old;
            }
        }

        // 当前索引如果没有对象
        addEntry(hash, key, value, index);
        return null;
    }

	/**
     * 添加entry
     * @param hash
     * @param key
     * @param value
     * @param index
     */
    private void addEntry(int hash, K key, V value, int index) {
        // 修改次数++
        modCount++;

        Hashtable.Entry<?,?> tab[] = table;
        // 判断元素数量是否大于临界值
        if (count >= threshold) {
            // 执行扩容方法
            rehash();
            // 修改之后的table赋值给成员变量
            tab = table;
            // 执行扩容之后修改的hash
            hash = key.hashCode();
            // 计算新的索引
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        @SuppressWarnings("unchecked")
        // 将当前索引的对象赋值给对象e
        Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>) tab[index];
        // 将新的节点变为当前索引位置的节点,原先的节点作为新节点的next
        tab[index] = new Hashtable.Entry<>(hash, key, value, e);
        // 数量++
        count++;
    }
  • 扩容方法:
	/**
     * 扩容方法
     */
    protected void rehash() {
        // 记录当前table长度
        int oldCapacity = table.length;
        // 存储table值
        Hashtable.Entry<?,?>[] oldMap = table;

        // 新的容量为 原先容量 * 2  + 1
        int newCapacity = (oldCapacity << 1) + 1;
        // 如果新容量大于最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            // 如果原先容量已经是最大容量,无法再进行扩容,直接返回
            if (oldCapacity == MAX_ARRAY_SIZE)
                return;
            // 扩容到最大容量
            newCapacity = MAX_ARRAY_SIZE;
        }
        // 创建一个新容量大小的entry数组
        Hashtable.Entry<?,?>[] newMap = new Hashtable.Entry<?,?>[newCapacity];

        // 修改次数++
        modCount++;
        // 计算新的临界值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        // 把新的数组赋值给table
        table = newMap;

        // 循环赋值,将原先的table中的数据复制给新的table
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Hashtable.Entry<K,V> old = (Hashtable.Entry<K,V>)oldMap[i]; old != null ; ) {
                // 将以链表形式存在的节点也重新赋值
                Hashtable.Entry<K,V> e = old;
                old = old.next;
				
				// 重新计算hash
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Hashtable.Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

在这里插入图片描述

1.3 Properties

也是以键值对方式存储,主要用于配置文件。
key和value都不可以用null
没啥可写的。

1.4 TreeSet和TreeMap

最大的特点是有序且唯一(set值唯一,map的key唯一 )的,可以默认排序,也可以指定排序规则,最强的是根据规则可以指定是否能加入。

1.4.1 构造函数

实际上TreeSet的底层用的是TreeMap。也就意味着他俩的构造函数都是调用的TreeMap的。

  • 无参构造
public TreeSet() {
        this(new TreeMap<E,Object>());
    }
  • 传入比较器
 public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

指定排序规则

int compare(T o1, T o2);

1.4.2 添加和扩容机制

  • 添加元素
	/**
     * 添加元素
     * @param e 元素
     * @return true / false
     */
    public boolean add(E e) {
    	// 这里实际上调用的是TreeMap.put方法
        return m.put(e, PRESENT)==null;
    }

2022.04.04 没有学习树的相关,姑且暂做分析。

    /**
     * 添加方法
     * @param key k
     * @param value v
     * @return v
     */
    public V put(K key, V value) {
    	// 获取根节点
        TreeMap.Entry<K,V> t = root;
        // 如果根节点等于空直接加入
        if (t == null) {
            compare(key, key); 
            root = new TreeMap.Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        // 记录父母节点
        TreeMap.Entry<K,V> parent;
        // 将传入的比较器赋值
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do { 
                parent = t;
                // 判断比较规则返回的值
                cmp = cpr.compare(key, t.key);
                // 小就和父节点的左边比较,大就和右边比较
                // 如果相等直接返回 0 :TreeSet(无法加入)、(TreeMap会执行替换)
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);// 从根节点依次往下找。
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            // 默认比较器
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 新建节点对象
        TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
        // 根据判断的大小排序,确定放在左边或者右边
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

完结了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值