freyhe

导航

09-2.java容器类之Map

Map集合

Map接口概述

image-20220216221657353

现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即java.util.Map接口。

我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同:

  • Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  • Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  • Collection中的集合称为单列集合,Map中的集合称为双列集合。
  • 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

Map关系图总览

|----Map:双列数据,存储key-value对的数据 ---类似于高中的函数:y = f(x)
|----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
|----LinkedHashMap:该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率,并通过链表确保了元素存取的有序性
原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap。
|----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序进行排序
底层使用红黑树,需要使用Comparable或者Comparator 进行比较排序。return 0,来判断键的唯一性。
|----Hashtable:作为古老的实现类;线程安全的(同步锁),效率低;不能存储null的key和value
|----Properties:常用来处理配置文件。key和value都是String类型

image-20220216221730630

1. Map常见子类

通过查看Map接口描述,看到Map有多个子类,这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。

HashMap

HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

image-20220307204306335

image-20220307204427475

image-20220307204537431

LinkedHashMap

image-20220216221818681

LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。

TreeMap

image-20220216221918899

Hashtable

image-20220216222006029

Properties

Properties集合是一个唯一和IO流相结合的集合

image-20220216222024027

使用Properties读取配置文件
public static void main(String[] args)  {
    FileInputStream fis = null;
    try {
        Properties pros = new Properties();

        fis = new FileInputStream("jdbc.properties");
        pros.load(fis);//加载流对应的文件

        String name = pros.getProperty("name");
        String password = pros.getProperty("password");

        System.out.println("name = " + name + ", password = " + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

2. Map常见方法

Map接口中定义了很多方法,常用的如下:

1、添加:

​ V put(K key, V value) (可以相同的key值,但是添加的value值会覆盖前面的,返回值是前一个,如果没有就返回null)

​ putAll(Map<? extends K,? extends V> m) 从指定映射中将所有映射关系复制到此映射中(可选操作)。

2、删除

​ remove() 删除关联对象,指定key对象

​ clear() 清空集合对象

3、获取

​ value get(key); 可以用于判断键是否存在的情况。当指定的键不存在的时候,返回的是null。

3、判断:

​ boolean isEmpty() 长度为0返回true否则false

​ boolean containsKey(Object key) 判断集合中是否包含指定的key

​ boolean containsValue(Object value) 判断集合中是否包含指定的value

4、长度:

Int size()

5.元视图操作的方法

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

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

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

3. Map集合的特点

Map集合的特点:
    1.Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
    2.Map集合中的元素,key和value的数据类型可以相同,也可以不同
    3.Map集合中的元素,key是不允许重复的,value是可以重复的
    4.Map集合中的元素,key和value是一一对应

java.util.HashMap<k,v>集合 implements Map<k,v>接口
HashMap集合的特点:
    1.HashMap集合底层是哈希表:查询的速度特别的快
        JDK1.8之前:数组+单向链表
        JDK1.8之后:数组+单向链表|红黑树(链表的长度超过8且数据长度超过64):提高查询的速度    
    2.hashMap集合是一个无序的集合,存储元素和取出元素的顺序有可能不一致

java.util.LinkedHashMap<k,v>集合 extends HashMap<k,v>集合
   LinkedHashMap的特点:
    1.LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
    2.LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的

4. Map的遍历

Map的遍历主要有三种方式:

  1. 遍历所有的value.这种场景很少使用,因为Map是成对出现的,只遍历value意义不大。
  2. 根据key查找到对应的value(不推荐,遍历的效率相对较低)
  3. 遍历获取到一个一个EntrySet类型的键值对,然后再获取键值对里的keyvalue.
Map<String, String> student = new HashMap<String, String>();
student.put("姓名", "张三");
student.put("年龄", "18");
student.put("性别", "男");
student.put("身高", "180cm");

// 第一种方式:获取所有的value组成的set
Collection<String> values = student.values();
for (String value : values) {  // 遍历集合,取到每一个value
    System.out.println(value);
}

// 第二种方式:获取到所有的key组成的set
Set<String> keys = student.keySet();  // 获取所有的key组成的set
for (String key : keys) {  // 遍历所有的 key
    // 调用 Map 的 get 方法,获取到对应的value
    System.out.println(key + " = " + student.get(key));
}

// 第三种方式:
Set<Map.Entry<String, String>> entries = student.entrySet();
for (Map.Entry<String, String> entry : entries) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

image-20220307210106828

5. Entry对象

我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。

既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:

  • public K getKey():获取Entry对象中的键。
  • public V getValue():获取Entry对象中的值。

在Map集合中也提供了获取所有Entry对象的方法:

  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

6. Map存储自定义类型对象

练习:学生对象有姓名,年龄属性,同时,每个学生对象都有一个唯一的ID和他一一对应。要求以HashMap的形式保存保存多个学生对象。

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("李四", 20);
        Student s3 = new Student("jack", 19);
        Student s4 = new Student("张三", 18);



        // 以字符串作为 key,以 Student 类型对象作为 value
        // Map<String, Student> students = new HashMap<String, Student>();
        // students.put("0001", s1);
        // students.put("0002", s2);
        // students.put("0003", s3);
        // students.put("0004", s4);
        // students.put("0005", s1);

        // 如果要以 Student 对象作为 key,以 字符串为 value呢?
        Map<Student, String> students = new HashMap<Student, String>();
        students.put(s1,"0001");
        students.put(s2,"0002");
        students.put(s3,"0003");

        // s4 和 s1 的name和age都相同,但是都会被放到Map里
        // 我们应该想办法,让解释器认为 name 和 age 都相等的 Student 是同一个对象
        // 此时需要重写 Student 类的 equals 和 hashCode 方法
        students.put(s4,"0004");
    }
}

class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Student) {
            return this.name == ((Student) obj).getName() && this.age == ((Student) obj).getAge();
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
  • 当给HashMap以自定义对象作为key,如果要保证key的唯一,必须复写对象的hashCode和equals方法

    (如果忘记,请回顾HashSet存放自定义对象)。

  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

7. HashMap源码浅析

HashMap添加元素时,执行的步骤:
    步骤1:计算key的二次哈希值
    步骤2:判断是否为第一次添加
            如果是第一次添加,则初始容量为16,临界值为12
            如果不是第一次添加,则继续下一步
    步骤3:根据二次哈希值,计算待添加的index(插入点)
            (n-1)&hash(key)

    步骤4:判断index处是否已有元素
        1、如果没有元素,则直接赋值
        2、如果已有元素,则按下面情况进行处理:
            ①.判断是否和当前元素相等,如果相等,直接覆盖
            ②.判断是否为树状结构,如果是,则按着树状结构进行处理
            ③.直接当做链表结构处理,依次判断next是否有元素,如果有,则判断是否相等,如果相等,则覆盖;如果不等,则继续往下判断。直到所有的都不相等,然后连接到最后
    步骤5:判断是否需要扩容,如果需要扩容,则扩容2倍
            oldCapacity<<1

            if(++size>threshold){
                resize();
            }

4.1 HashMap在jdk7中实现原理:

HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
  ...可能已经执行过多次put...
map.put(key1,value1):

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

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

  • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
    • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    • 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
      • 如果equals()返回false:此时key1-value1添加成功。----情况3
      • 如果equals()返回true:使用value1替换value2。

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

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。

默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。

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

1.new HashMap():底层没创建一个长度为16的数组(jdk8在第一次put时才创建组数)

2.jdk 8底层的数组是:Node[],而非Entry[]

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

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

​ jdk7:旧元素让位,新元素放在hash表头并指向旧元素;

​ jdk8:旧元素位置不动,而是指向新的元素,新元素等于加载旧元素下面

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

8. LinkedHashMap

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        map.put("邓超", "孙俪");
        map.put("鹿晗", "关晓彤");
        map.put("刘德华", "朱丽倩");
        Set<Entry<String, String>> entrySet = map.entrySet();
        for (Entry<String, String> entry : entrySet) {
            System.out.println(entry.getKey() + "  " + entry.getValue());
        }
    }
}

LinkedHashMap的底层实现原理

LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap.

区别就在于:LinkedHashMap内部提供了Entry(除了kv结构,还实现了链表的结构),替换HashMap中的Node(单纯的key-value结构)

	/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //key经过hash计算所得索引
        final K key;
        V value;
        Node<K,V> next;
    }
	/**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

9. HashMap和HashTable区别

HashMap:底层是一个哈希表,是一个线程不安全的集合,速度快
Hashtable:底层也是一个哈希表,是一个线程安全的集合,速度慢

HashMap集合:可以存储null值,null键
Hashtable集合,不能存储null值,null键

Hashtable和Vector集合一样,在jdk1.2版本之后被更先进的集合(HashMap,ArrayList)取代了

Hashtable的子类Properties(配置文件)依然活跃在历史舞台   xml
Properties集合是一个唯一和IO流相结合的集合

10. Map和Set关系

Set的内部实现其实是一个Map。即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。

部分源代码摘要:

HashSet源码:

public HashSet() {
    map = new HashMap<>();
}

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

//这个构造器是给子类LinkedHashSet调用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

LinkedHashSet源码:

public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);//调用HashSet的某个构造器
}

public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);//调用HashSet的某个构造器
}

public LinkedHashSet() {
    super(16, .75f, true);
}

public LinkedHashSet(Collection<? extends E> c) {
    super(Math.max(2*c.size(), 11), .75f, true);//调用HashSet的某个构造器
    addAll(c);
}

TreeSet源码:

public TreeSet() {
    this(new TreeMap<E,Object>());
}

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

但是,咱们存到Set中只有一个元素,又是怎么变成(key,value)的呢?

以HashSet中的源码为例:

private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

原来是,把添加到Set中的元素作为内部实现map的key,然后用一个常量对象PRESENT对象,作为value。

这是因为Set的元素不可重复和Map的key不可重复有相同特点。Map有一个方法keySet()可以返回所有key。

Collections类

1.作用:操作Collection和Map的工具类

​ Collections 是一个操作 Set、List 和 Map 等集合的工具类

​ Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,

​ Collections 中还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

2.常用方法:

排序操作:(均为static方法)

reverse(List):反转 List 中元素的顺序

shuffle(List):对 List 集合元素进行随机排序

sort(List):根据元素的自然顺序对指定 List 集合元素升序排序

sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

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

查找、替换

Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素

Object min(Collection)

Object min(Collection,Comparator)

int frequency(Collection,Object):返回指定集合中指定元素的出现次数

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

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

3.同步控制

ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程的。

使用synchronizedList(List list) 和 synchronizedMap(Map map)

image-20220308080134508

HashMap源码分析

存储到HashMap中的映射关系(key,value),其中的key的hashCode值和equals方法非常重要。

0.HashMap源码中的重要常量

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

MAXIMUM_CAPACITY HashMap的最大支持容量,2^30

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 ,0.75

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

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

UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表,6

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

当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,

此时应执行 resize扩容操作,确保MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。

table:存储元素的数组,总是2的n次幂 (先减一,按位与等于 1,2,4,8,16 |= 1 2 4 8 16 ,再加一 可得)

entrySet:存储具体元素的集

size:HashMap中存储的键值对的数量

modCount:HashMap扩容和结构改变的次数。

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

loadFactor:填充因子

1. HashCode值

hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。

image-20220307210145555

2. Hash表物理结构

HashMap和Hashtable是散列表,其中维护了一个长度为2的幂次方的Entry类型的数组table,数组的每一个元素被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到了某个table[index]桶中。使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。

2.1. 数组元素类型:Map.Entry

JDK1.7:

映射关系被封装为HashMap.Entry类型,而这个类型实现了Map.Entry接口。

观察HashMap.Entry类型是个结点类型,即table[index]下的映射关系可能串起来一个链表。因此我们把table[index]称为“桶bucket"。

public class HashMap<K,V>{
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;
            //...省略
    }
    //...
}

image-20220307210207214

JDK1.8:

映射关系被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。

存储到table数组的可能是Node结点对象,也可能是TreeNode结点对象,它们也是Map.Entry接口的实现类。即table[index]下的映射关系可能串起来一个链表或一棵红黑树(自平衡的二叉树)。

public class HashMap<K,V>{
    transient Node<K,V>[] table;
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
            //...省略
    }
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;
        boolean red;//是红结点还是黑结点
        //...省略
    }
    //....
}
public class LinkedHashMap<K,V>{
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    //...
}

image-20220307210231093

2.2. 数组的长度始终是2的n次幂

table数组的默认初始化长度:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

如果你手动指定的table长度不是2的n次幂,会通过如下方法给你纠正为2的n次幂

JDK1.7:

HashMap处理容量方法:

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

Integer包装类:

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

JDK1.8:

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

如果数组不够了,扩容了怎么办?扩容了还是2的n次幂,因为每次数组扩容为原来的2倍

JDK1.7:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);//扩容为原来的2倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}

JDK1.8:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap原来的容量
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }//newCap = oldCap << 1  新容量=旧容量扩容为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //......此处省略其他代码
}

那么为什么要保持table数组一直是2的n次幂呢?

2.3. HashMap映射关系

因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:

①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算,不能保证均匀存放,可能会导致某些table[index]桶中的元素太多,而另一些太少,因此不合适。

②hash 值 & (table.length-1),因为table.length是2的幂次方,因此table.length-1是一个二进制低位全是1的数,所以&操作完,也会得到一个[0,table.length-1]范围的值。

image-20220307210319846

JDK1.7:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1); //此处h就是hash
}

JDK1.8:

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;
    if ((p = tab[i = (n - 1) & hash]) == null)  // i = (n - 1) & hash
        tab[i] = newNode(hash, key, value, null);
    //....省略大量代码
}

2.4. hash是hashCode的再运算

不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。

JDK1.7:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8:

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

虽然算法不同,但是思路都是将hashCode值的高位二进制与低位二进制值进行了异或,然高位二进制参与到index的计算中。

为什么要hashCode值的二进制的高位参与到index计算呢?

因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。

2.5. 解决Index冲突问题

虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?

JDK1.8之间使用:数组+链表的结构。

JDK1.8之后使用:数组+链表/红黑树的结构。

即hash相同或hash&(table.lengt-1)的值相同,那么就存入同一个“桶”table[index]中,使用链表或红黑树连接起来。

image-20220307210348053

当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。

但是二叉树的结构又过于复杂,如果结点个数比较少的时候,那么选择链表反而更简单。

所以会出现红黑树和链表共存。

2.6. 树化和反树化

static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量
  • 当某table[index]下的链表的结点个数达到8,并且table.length>=64,那么如果新Entry对象还添加到该table[index]中,那么就会将table[index]的链表进行树化。
  • 当某table[index]下的红黑树结点个数少于6个,此时,
    • 如果继续删除table[index]下树结点,一直删除到2个以下时就会变回链表。
    • 如果继续添加映射关系到当前map中,如果添加导致了map的table重新resize,那么只要table[index]下的树结点仍然<=6个,那么会变回链表
class MyKey{
    int num;

    public MyKey(int num) {
        super();
        this.num = num;
    }

    @Override
    public int hashCode() {
        if(num<=20){
            return 1;
        }else{
            final int prime = 31;
            int result = 1;
            result = prime * result + num;
            return result;            
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyKey other = (MyKey) obj;
        if (num != other.num)
            return false;
        return true;
    }

}
public class TestHashMap {

    @Test
    public void test1(){
        //这里为了演示的效果,我们造一个特殊的类,这个类的hashCode()方法返回固定值1
        //因为这样就可以造成冲突问题,使得它们都存到table[1]中
        HashMap<MyKey, String> map = new HashMap<>();
        for (int i = 1; i <= 11; i++) {
            map.put(new MyKey(i), "value"+i);//树化演示
        }
    }
   @Test
    public void test2(){
        HashMap<MyKey, String> map = new HashMap<>();
        for (int i = 1; i <= 11; i++) {
            map.put(new MyKey(i), "value"+i);
        }
        for (int i = 1; i <=11; i++) {
            map.remove(new MyKey(i));//反树化演示
        }
    }
    @Test
    public void test3(){
        HashMap<MyKey, String> map = new HashMap<>();
        for (int i = 1; i <= 11; i++) {
            map.put(new MyKey(i), "value"+i);
        }

        for (int i = 1; i <=5; i++) {
            map.remove(new MyKey(i));
        }//table[1]下剩余6个结点

        for (int i = 21; i <= 100; i++) {
            map.put(new MyKey(i), "value"+i);//添加到扩容时,反树化
        }
    }

3. JDK1.7源码分析

(1)几个关键的常量和变量值的作用:

初始化容量:

int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16

①默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

②阈值:扩容的临界值

int threshold;
threshold = table.length * loadFactor;

③负载因子

final float loadFactor;

负载因子的值大小有什么关系?

如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。

如果太小,threshold就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。

    public HashMap() {
        //DEFAULT_INITIAL_CAPACITY:默认初始容量16
        //DEFAULT_LOAD_FACTOR:默认加载因子0.75
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {
        //校验initialCapacity合法性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
        //校验initialCapacity合法性                                       initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //校验loadFactor合法性
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //加载因子,初始化为0.75
        this.loadFactor = loadFactor;
        // threshold 初始为初始容量                                  
        threshold = initialCapacity;
        init();
    }
public V put(K key, V value) {
        //如果table数组是空的,那么先创建数组
        if (table == EMPTY_TABLE) {
            //threshold一开始是初始容量的值
            inflateTable(threshold);
        }
        //如果key是null,单独处理
        if (key == null)
            return putForNullKey(value);

        //对key的hashCode进行干扰,算出一个hash值
        int hash = hash(key);

        //计算新的映射关系应该存到table[i]位置,
        //i = hash & table.length-1,可以保证i在[0,table.length-1]范围内
        int i = indexFor(hash, table.length);

        //检查table[i]下面有没有key与我新的映射关系的key重复,如果重复替换value
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //添加新的映射关系
        addEntry(hash, key, value, i);
        return null;
    }
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);//容量是等于toSize值的最接近的2的n次方
        //计算阈值 = 容量 * 加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建Entry[]数组,长度为capacity
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
    //如果key是null,直接存入[0]的位置
    private V putForNullKey(V value) {
        //判断是否有重复的key,如果有重复的,就替换value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //把新的映射关系存入[0]的位置,而且key的hash值用0表示
        addEntry(0, null, value, 0);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //判断是否需要库容
        //扩容:(1)size达到阈值(2)table[i]正好非空
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //table扩容为原来的2倍,并且扩容后,会重新调整所有映射关系的存储位置
            resize(2 * table.length);
            //新的映射关系的hash和index也会重新计算
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //存入table中
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //原来table[i]下面的映射关系作为新的映射关系next
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;//个数增加
    }

1、put(key,value)

(1)当第一次添加映射关系时,数组初始化为一个长度为16HashMap$Entry的数组,这个HashMap$Entry类型是实现了java.util.Map.Entry接口

(2)特殊考虑:如果key为null,index直接是[0],hash也是0

(3)如果key不为null,在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中

(4)计算index = table.length-1 & hash;

(5)如果table[index]下面,已经有映射关系的key与我要添加的新的映射关系的key相同了,会用新的value替换旧的value。

(6)如果没有相同的,会把新的映射关系添加到链表的头,原来table[index]下面的Entry对象连接到新的映射关系的next中。

(7)添加之前先判断if(size >= threshold && table[index]!=null)如果该条件为true,会扩容

if(size >= threshold  &&  table[index]!=null){

    ①会扩容

    ②会重新计算key的hash

    ③会重新计算index

}

image-20220307210453193

2、get(key)

(1)计算key的hash值,用这个方法hash(key)

(2)找index = table.length-1 & hash;

(3)如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就返回它的value

3、remove(key)

(1)计算key的hash值,用这个方法hash(key)

(2)找index = table.length-1 & hash;

(3)如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就删除它,把它前面的Entry的next的值修改为被删除Entry的next

4. JDK1.8的put方法源码分析

几个常量和变量:
(1)DEFAULT_INITIAL_CAPACITY:默认的初始容量 16
(2)MAXIMUM_CAPACITY:最大容量  1 << 30
(3)DEFAULT_LOAD_FACTOR:默认加载因子 0.75
(4)TREEIFY_THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
(5)UNTREEIFY_THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
(6)MIN_TREEIFY_CAPACITY:最小树化容量64
        当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
        当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
(7)Node<K,V>[] table:数组
(8)size:记录有效映射关系的对数,也是Entry对象的个数
(9)int threshold:阈值,当size达到阈值时,考虑扩容
(10)double loadFactor:加载因子,影响扩容的频率
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
    // all other fields defaulted,其他字段都是默认值
}
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//目的:干扰hashCode值
static final int hash(Object key) {
    int h;
    //如果key是null,hash是0
    //如果key非null,用key的hashCode值 与 key的hashCode值高16进行异或
    //        即就是用key的hashCode值高16位与低16位进行了异或的干扰运算

    /*
        index = hash & table.length-1
        如果用key的原始的hashCode值  与 table.length-1 进行按位与,那么基本上高16没机会用上。
        这样就会增加冲突的概率,为了降低冲突的概率,把高16位加入到hash信息中。
        */
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; //数组
    Node<K,V> p; //一个结点
    int n, i;//n是数组的长度   i是下标

    //tab和table等价
    //如果table是空的
    if ((tab = table) == null || (n = tab.length) == 0){
        n = (tab = resize()).length;
        /*
            tab = resize();
            n = tab.length;*/
        /*
            如果table是空的,resize()完成了①创建了一个长度为16的数组②threshold = 12
            n = 16
            */
    }
    //i = (n - 1) & hash ,下标 = 数组长度-1 & hash
    //p = tab[i] 第1个结点
    //if(p==null) 条件满足的话说明 table[i]还没有元素
    if ((p = tab[i = (n - 1) & hash]) == null){
        //把新的映射关系直接放入table[i]
        tab[i] = newNode(hash, key, value, null);
        //newNode()方法就创建了一个Node类型的新结点,新结点的next是null
    }else {
        Node<K,V> e; 
        K k;
        //p是table[i]中第一个结点
        //if(table[i]的第一个结点与新的映射关系的key重复)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))){
            e = p;//用e记录这个table[i]的第一个结点
        }else if (p instanceof TreeNode){//如果table[i]第一个结点是一个树结点
            //单独处理树结点
            //如果树结点中,有key重复的,就返回那个重复的结点用e接收,即e!=null
            //如果树结点中,没有key重复的,就把新结点放到树中,并且返回null,即e=null
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        }else {
            //table[i]的第一个结点不是树结点,也与新的映射关系的key不重复
            //binCount记录了table[i]下面的结点的个数
            for (int binCount = 0; ; ++binCount) {
                //如果p的下一个结点是空的,说明当前的p是最后一个结点
                if ((e = p.next) == null) {
                    //把新的结点连接到table[i]的最后
                    p.next = newNode(hash, key, value, null);

                    //如果binCount>=8-1,达到7个时
                    if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
                        //要么扩容,要么树化
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                //如果key重复了,就跳出for循环,此时e结点记录的就是那个key重复的结点
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))){
                    break;
                }
                p = e;//下一次循环,e=p.next,就类似于e=e.next,往链表下移动
            }
        }
        //如果这个e不是null,说明有key重复,就考虑替换原来的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null){
                e.value = value;
            }
            afterNodeAccess(e);//什么也没干
            return oldValue;
        }
    }
    ++modCount;

    //元素个数增加
    //size达到阈值
    if (++size > threshold){
        resize();//一旦扩容,重新调整所有映射关系的位置
    }
    afterNodeInsertion(evict);//什么也没干
    return null;
}    

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//oldTab原来的table
    //oldCap:原来数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //oldThr:原来的阈值
    int oldThr = threshold;//最开始threshold是0

    //newCap,新容量
    //newThr:新阈值
    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){
            //newCap = 旧的容量*2 ,新容量<最大数组容量限制
            //新容量:32,64,...
            //oldCap >= 初始容量16
            //新阈值重新算 = 24,48 ....
            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;//新容量是默认初始化容量16
        //新阈值= 默认的加载因子 * 默认的初始化容量 = 0.75*16 = 12
        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;//阈值赋值为新阈值12,24.。。。

    //创建了一个新数组,长度为newCap,16,32,64.。。
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;


    if (oldTab != null) {//原来不是空数组
        //把原来的table中映射关系,倒腾到新的table中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {//e是table下面的结点
                oldTab[j] = null;//把旧的table[j]位置清空
                if (e.next == null)//如果是最后一个结点
                    newTab[e.hash & (newCap - 1)] = e;//重新计算e的在新table中的存储位置,然后放入
                else if (e instanceof TreeNode)//如果e是树结点
                    //把原来的树拆解,放到新的table
                    ((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;
                    /*
                        把原来table[i]下面的整个链表,重新挪到了新的table中
                        */
                    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;
}    

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    //创建一个新结点
    return new Node<>(hash, key, value, next);
}

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; 
    Node<K,V> e;
    //MIN_TREEIFY_CAPACITY:最小树化容量64
    //如果table是空的,或者  table的长度没有达到64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//先扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //用e记录table[index]的结点的地址
        TreeNode<K,V> hd = null, tl = null;
        /*
            do...while,把table[index]链表的Node结点变为TreeNode类型的结点
            */
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;//hd记录根结点
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);

        //如果table[index]下面不是空
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//将table[index]下面的链表进行树化
    }
}    

1、添加过程

(1)当第一次添加映射关系时,数组初始化为一个长度为16HashMap$Node的数组,这个HashMap$Node类型是实现了java.util.Map.Entry接口

(2)在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中

JDK1.8关于hash(key)方法的实现比JDK1.7要简洁。 key.hashCode() ^ key.Code()>>>16;

(3)计算index = table.length-1 & hash;

(4)如果table[index]下面,已经有映射关系的key与我要添加的新的映射关系的key相同了,会用新的value替换旧的value。

(5)如果没有相同的,

①table[index]链表的长度没有达到8个,会把新的映射关系添加到链表的尾

②table[index]链表的长度达到8个,但是table.length没有达到64,会先对table进行扩容,然后再添加

③table[index]链表的长度达到8个,并且table.length达到64,会先把该分支进行树化,结点的类型变为TreeNode,然后把链表转为一棵红黑树

④table[index]本来就已经是红黑树了,那么直接连接到树中,可能还会考虑考虑左旋右旋以保证树的平衡问题

(6)添加完成后判断if(size > threshold ){

    ①会扩容

    ②会重新计算key的hash

    ③会重新计算index

}

image-20220307210529417

2、remove(key)

(1)计算key的hash值,用这个方法hash(key)

(2)找index = table.length-1 & hash;

(3)如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就删除它,把它前面的Entry的next的值修改为被删除Entry的next

(4)如果table[index]下面原来是红黑树,结点删除后,个数小于等于6,会把红黑树变为链表

5. 映射关系的key是否可以修改

映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。

JDK1.7:

public class HashMap<K,V>{
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash; //记录Entry映射关系的key的hash(key.hashCode())值
            //...省略
    }
    //...
}

JDK1.8:

public class HashMap<K,V>{
    transient Node<K,V>[] table;
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;//记录Node映射关系的key的hash(key.hashCode())值
            final K key;
            V value;
            Node<K,V> next;
            //...省略
    }
    //....
}

示例代码:

import java.util.HashMap;

public class TestHashMap {
    public static void main(String[] args) {
        HashMap<ID,String> map = new HashMap<>();
        ID i1 = new ID(1);
        ID i2 = new ID(2);
        ID i3 = new ID(3);

        map.put(i1, "haha");
        map.put(i2, "hehe");
        map.put(i3, "xixi");

        System.out.println(map.get(i1));//haha
        i1.setId(10);
        System.out.println(map.get(i1));//null
    }
}
class ID{
    private int id;

    public ID(int id) {
        super();
        this.id = id;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ID other = (ID) obj;
        if (id != other.id)
            return false;
        return true;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

}

所以实际开发中,经常选用String,Integer等作为key,因为它们都是不可变对象。

posted on 2022-03-07 21:34  freyhe  阅读(48)  评论(0编辑  收藏  举报