Java基础学习总结:集合之(二)Map、HashMap、TreeMap

一、Map

Java为我们提供了两大类可用的集合,一类是单列集合(Collection),另一类就是双列集合(Map)。Map接口是将映射到值的对象。一个映射不能包含重复的键,每个键最多只能映射一个值。为什么在学习Set集合之前,要先学习Map集合呢,因为Set集合的底层实现,很多都是借助Map集合来实现的。

(1)Map集合中常用的方法

方法名描述

clear()

从此映射中移除所有映射关系

containsKey(Object key)

如果此映射包含指定键的映射关系,则返回true

containsValue(Object value)

如果此映射将一个或多个键映射到指定值,则返回true

entrySet()

返回此映射中包含的映射关系的Set视图

equals(Object o)

比较指定的对象与此映射是否相等

get(Object key)

返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回null。

hashCode()

返回此映射的哈希码值

isEmpty()

如果此映射包含键-值映射关系,则返回true

keySet()

返回此映射中包含的键的Set视图

put(K key, V value)

将指定的值与此映射中的指定键关联

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

从指定映射中将所有映射关系复制到此映射中

remove(Object key)

如果存在一个键的映射关系,则将其从此映射中移除

size()

返回此映射中的键-值关系的个数

双列集合 Map 接口有两个重要的实现类,HashMap 和 TreeMap

二、HashMap

(1)简介

HashMap实现原理:数组+链表(jdk1.7及之前),数组+链表+红黑树(jdk1.8)

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
  • HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
  • HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
  • HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
  • 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

(2)HashMap的构造函数

HashMap共有4个构造函数,如下:

构造方法描述
HashMap()默认构造函数。
HashMap(int capacity)指定“容量大小”的构造函数
HashMap(int capacity, float loadFactor)指定“容量大小”和“加载因子”的构造函数
HashMap(Map<? extends K, ? extends V> map)包含“子Map”的构造函数

(3)HashMap的使用

package basis.MapStu;

import java.util.HashMap;
import java.util.Map;

public class TestHashMap {
    public static void main(String[] args) {
        //创建一个HashMap集合
        HashMap<String,String> hashMap = new HashMap();
        //添加映射关系,如果键重复,则把已存在的映射关系覆盖
        hashMap.put("001","张三");
        hashMap.put("002","李四");
        hashMap.put("003","王五");
        hashMap.put("003","赵六");

        //映射关系的个数
        System.out.println(hashMap.size());//3

        //是否包含指定键
        System.out.println(hashMap.containsKey("001"));//true
        //是否存在一个或多个键映射到该值
        System.out.println(hashMap.containsValue("王五"));//false

        //比较两个映射对象是否相等
        HashMap<String,String> hashMap1 = new HashMap<>();
        hashMap1.put("004","冰冰");
        System.out.println(hashMap.equals(hashMap1));//false

        //查找指定键所映射的值,如果不包含该键的映射关系,返回null
        System.out.println(hashMap.get("003"));//赵六

        //此映射的hashcode值
        System.out.println(hashMap.hashCode());

        //判断此映射是否为空
        System.out.println(hashMap.isEmpty());//false

        //将指定映射的所有映射关系添加到此映射
        hashMap.putAll(hashMap1);
        //删除指定键的映射关系,并返回该映射关系的值,如果不存在返回null
        System.out.println(hashMap.remove("004"));

        //遍历Map集合
        for (Map.Entry e : hashMap.entrySet()){
            System.out.println(e.getKey()+":"+e.getValue());
        }
    }
}

三、HashMap源码分析

(1)HashMap的继承关系

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { }

小插曲:为什么 HashMap 继承了 AbstractMap 还要去实现 Map接口呢???(这样的写法在集合框架里非常多)

答案一:动态代理中会用到 Class 的 getInterface() ,此方法用于获取该类实现的接口,但不能直接获取父类实现的接口。但是动态代理出生在jdk1.3,而集合框架在jdk1.2中就有了。

答案二:有人声称询问过java集合框架的创始人Josh Bloch,Josh Bloch承认这是一个错误的写法。

类图

Map中的常量:

//HashMap的初始容量默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//使用红黑树结构时,链表长度阙值为8
static final int TREEIFY_THRESHOLD = 8;
//改回链表时,链表长度阙值为6
static final int UNTREEIFY_THRESHOLD = 6;
//使用红黑树结构时,集合最小容量为64
static final int MIN_TREEIFY_CAPACITY = 64;

内部接口Entry:

Entry是 Map 接口内部的一个用于存放、获取和设置映射关系的键和值的接口

interface Entry<K,V> {

        K getKey();

        V getValue();

        V setValue(V value);

        boolean equals(Object o);


        int hashCode();

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


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

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


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

内部类Node:

Node结点是 HashMap 中用于存放映射关系的内部类,实现了Map接口中的 Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return 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;
        }

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

put() 方法:

前面提到过,put()方法用于向映射中添加指定的映射关系,在该方法中调用了 putVal() 方法,并将键的Hash值作为参数传进去。

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

putVal() 方法:

原文:https://blog.csdn.net/li_cangqiong/article/details/81871332

/**
     * Implements Map.put and related methods
     *
     * @param hash 根据静态方法hash获得的hash值
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent if true,当键相同时,不修改已存在的值
     * @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;
		//定义一个临时链表数组,对hashmap中的table进行操作,n是table的长度,为hashmap初始化时定义的值,是2的幂,i为tab的游标
        if ((tab = table) == null || (n = tab.length) == 0)//如果这个hashmap是空的
            n = (tab = resize()).length;//就resize一下这个hashmap,获取容量
        if ((p = tab[i = (n - 1) & hash]) == null)//这行代码的意思是获取tab链表数组中的第length个链表,如果链表为空,就创建新节点添加进去,它的nextnode为null
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//如果hash相同,键相同,就直接把p指向e
                e = p;
            else if (p instanceof TreeNode)//如果p节点类型为红黑树,就添加到红黑树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {//链表数组中如果不出现hash碰撞,最完美的情况应该是每个链表都是单结点,这里统计链表中节点的个数
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);//把新来的结点加入到这条链表的最后
                        if (binCount >= TREEIFY_THRESHOLD - 1) //如果链表中节点个数大于TREEIFY_THRESHOLD-1,也就是7,为什么是7呢,因为第一个节点不算进去,所以应该算是8个结点了
                            treeifyBin(tab, hash);//把这个链表转化成红黑树,然后直接退出循环
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//在结点遍历的过程中,如果有hash值相同的情况,且key值相同,就直接退出循环,把这个找到的结点直接赋值
                        break;
                    p = e;//每次都指向下一个结点
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)//在onlyIfAbsent为false时,可以覆盖键值对,或者onlyIfAbesent为true但是value为空时也可以覆盖
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;//返回的是e.value的指向
            }
        }
        ++modCount;//操作数+1
        if (++size > threshold)//如果链表数组大小大于了阈值,就扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

解释一下

if ((p = tab[i = (n - 1) & hash]) == null)

hash()方法: 用于计算键的hash值

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hashcode(): hashcode是Object类的一个本地方法,获取键的hash码。

public native int hashCode();

获得的hashcode是一个32位的int型数据,很明显从2的-31次幂到2的32次幂长度的数据远超过hashmap的最大容量,所以这里通过key的hashcode右移16位与原hashcode进行异或运算,实际上就是hashcode与前16位换成0的hashcode进行或非运算,如果高位不参与运算,发生hash冲撞的几率就会增大,从而影响性能。

至于为什么用n-1,举个例子,假设n为16,n-1为15,也就是0000 0000 0000 0000 0000 0000 0000 1111,和hash值进行与运算,也就是这后四位进行运算,结果就被限制在0000~1111之间,正好是数组的长度,游标从0到15。

四、TreeMap

(1)简介

  • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
  • TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
  • TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
  • TreeMap 实现了Cloneable接口,意味着它能被克隆
  • TreeMap 实现了java.io.Serializable接口,意味着它支持序列化
  • TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
  • TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

(2)构造方法

构造方法描述
TreeMap()默认构造函数。
public TreeMap(Comparator<? super K> comparator)指定TreeMap的比较器
public TreeMap(Map<? extends K, ? extends V> m)包含“子Map”的构造函数
public TreeMap(SortedMap<K, ? extends V> m)创建一个TreeMap包含copyFrom

(3)继承关系:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{}

 (4)TreeMap类图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值