Java 集合
Posted Mr.Aaron
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合相关的知识,希望对你有一定的参考价值。
集合的由来及集合继承体系图
集合的由来
- 数组长度是固定,当添加的元素超过了数组的长度时需要对数组重新定义
- java内部给我们提供了集合类,能存储任意对象,长度是可以改变的,随着元素的增加而增加,随着元素的减少而减少
数组和集合的区别
区别1 :
- 数组既可以存储基本数据类型,又可以存储引用数据类型,基本数据类型存储的是值,引用数据类型存储的是地址值
- 集合只能存储引用数据类型(对象),集合中也可以存储基本数据类型,但是在存储的时候会自动装箱变成对象,int - Integer
区别2:
- 数组长度是固定的,不能自动增长
- 集合的长度的是可变的,可以根据元素的增加而增长
数组和集合什么时候用
- 如果元素个数是固定的推荐用数组
- 如果元素个数不是固定的推荐用集合
集合的由来及集合继承体系图
- List:有序集合,有索引,存与取的顺序一样,可以重复
- Set:无序集合,无索引,存与取的顺序不一样,不可以重复
集合(Collection)继承体系图
集合中部分数组的实现原理
有个10容量的初始化数组,不够时,再搞个1.5倍的新数组,把以前的数组垃圾回收
Collection集合的基本功能
基本功能方法
- boolean add(E e)
- boolean remove(Object o)
- void clear()
- boolean contains(Object o)
- boolean isEmpty()
- int size()
集合的遍历之集合转数组遍历
集合的遍历
其实就是依次获取集合中的每一个元素。
案例演示
把集合转成数组,可以实现集合的遍历 toArray()
import java.util.ArrayList; import java.util.List; public class Demo01 { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); String[] arr = new String[3]; list.toArray(arr); } }
集合的遍历之迭代器遍历
迭代器概述
迭代器是用来遍历集合的每一个元素的
迭代器的使用
使用迭代器遍历ArrayList集合
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Ch05 { public static void main(String[] args) { List list = new ArrayList<>(); //集合 list.add(1); list.add(2); list.add(3); //Iterator迭代器 //1、获取迭代器 Iterator iter = list.iterator(); //2、通过循环迭代 //hasNext():判断是否存在下一个元素 while(iter.hasNext()){ //如果存在,则调用next实现迭代 //Object-->Integer-->int int j=(int)iter.next(); //把Object型强转成int型 System.out.println(j); } } }
ArrayList 的实现原理
ArrayList 概述
ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。
每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList实例的容量,这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。
ArrayList 的实现
对于 ArrayList 而言,它实现 List 接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。
1) 底层使用数组实现
private transient Object[] elementData;
2) 构造方法
ArrayList 提供了三种方式的构造器,可以构造一个默认初始容量为 10 的空列表、构造一个指定初始容量的空列表以及构造一个包含指定 collection 的元素的列表,这些元素按照该 collection 的迭代器返回它们的顺序排列的。
public ArrayList() { this(10); } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
3) 存储:
ArrayList 提供了 set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。 public E set(int index, E element) { RangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; }
// 将指定的元素添加到此列表的尾部。 public boolean add(E e) { ensureCapacity(size + 1); elementData[size++] = e; return true; }
// 将指定的元素插入此列表中的指定位置。 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加 1)。 public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size); // 如果数组长度不足,将进行扩容。 ensureCapacity(size+1); // Increments modCount!! // 将 elementData 中从 Index 位置开始、长度为 size-index 的元素, // 拷贝到从下标为 index+1 位置开始的新的 elementData 数组中。 // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
// 按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }
// 从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。 public boolean addAll(int index, Collection<? extends E> c) { if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
4) 读取
// 返回此列表中指定位置上的元素。 public E get(int index) { RangeCheck(index); return (E) elementData[index]; }
5) 删除
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。
// 移除此列表中指定位置上的元素。 public E remove(int index) { RangeCheck(index); modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Let gc do its work return oldValue; }
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。 public boolean remove(Object o) { // 由于 ArrayList 中允许存放 null,因此下面通过两种情况来分别处理。 if (o == null) { for (int index = 0; index < size; index++){ if (elementData[index] == null) { // 类似 remove(int index),移除列表中指定位置上的元素。 fastRemove(index); return true; }
} } else { for (int index = 0; index < size; index++){ if (o.equals(elementData[index])) { fastRemove(index); return true; }
} } return false; }
注意:从数组中移除元素的操作,也会导致被移除的元素以后的所有元素的向左移动一个位置。
6) 调整数组容量
从上面向 ArrayList 中存储元素的代码中,我们看到每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法 ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用 ensureCapacity 来手动增加 ArrayList 实例的容量,以减少递增式再分配的数量。
public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1; if (newCapacity < minCapacity){ newCapacity = minCapacity;
} // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } }
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的 1.5 倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用 ensureCapacity 方法来手动增加 ArrayList 实例的容量。
ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过 trimToSize 方法来实现。代码如下:
public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); } }
7) Fail-Fast 机制:
ArrayList 也采用了快速失败的机制,通过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
Vector的使用
Vector在JDK1.0 版本就有了,从 Java 2 平台 v1.2 开始,此类改进为可以实现 List 接口,使它成为 Java Collections Framework 的成员,Vector 是同步的。
Vector类特有功能
- public void addElement(E obj)
- public E elementAt(int index)
- public Enumeration elements()
数据结构之数组和链表特点
数组
- 查询快、修改也快
- 增删慢
链表
- 查询慢,修改也慢
- 增删快
List的三个子类的特点
ArrayList
- 底层数据结构是数组,查询快,增删慢。
- 线程不安全,效率高。
Vector
- 底层数据结构是数组,查询快,增删慢。
- 线程安全,效率低。
- Vector相对ArrayList查询慢(线程安全的)
- Vector相对LinkedList增删慢(数组结构)
LinkedList
- 底层数据结构是链表,查询慢,增删快。
- 线程不安全,效率高。
list总结
Vector和ArrayList的区别
- Vector是线程安全的,效率低 ArrayList是线程不安全的,效率高
- 共同点:都是数组实现的
ArrayList和LinkedList的区别
- ArrayList底层是数组实现,查询和修改快
- LinkedList底层是链表结构实现,增和删比较快,查询和修改比较慢
- 共同点:都是线程不安全的
List有三个子类到底使用谁呢?
- 查询多用ArrayList
- 增删多用LinkedList
- 如果都多ArrayList
泛型概述和基本使用
泛型概述(Generic)
- 泛型的作用:把类型明确的工作推前到创建对象或者调用方法的时候。
- 泛型是一种参数化类型,把类型当作参数一样传递来明确集合的元素类型
泛型好处
- 提高安全性(将运行期的错误转换到编译期)
- 省去强转的麻烦
泛型基本使用
- 声明集合泛型的格式List<Student> list = new ArrayList<Student>();
- <>中放的必须是引用数据类型
泛型使用注意事项
1.默认声明一个泛型集合,前后类型要一至
List<Student> list = new ArrayList<Student>();
2.这样声明前后类型不一至是不可以的
List<Object> list = new ArrayList<Student>();
3.集合泛型的声明,可以只声明前面的泛型,jdk1.7的新特性:菱形泛型,开发时建议还是写成前后一至
List<Student> list1 = new ArrayList();
4.集合声明的泛型,代表此类或者子类都可以成为集合的元素,eg: Person -> Student
5.声明的泛型类型一定是引用数据类型
泛型类和泛型方法
泛型类概述<T>
- 把泛型定义在类上
定义格式
- public class 类名<泛型类型1,…>
泛型类型注意事项
- 泛型类型必须是引用类型
- T的值是什么时候有的?是在创建对象时
泛型方法
- 泛型方法,把泛型定义在方法上
- 定义格式: public 返回类型 方法名(泛型类型 变量名)
- 对象方法的泛型参数要与类中的泛型一致,不可以使用其它名
泛型高级之通配符
泛型通配符<?>
任意类型,如果没有明确,那么就是Object以及任意的Java类了
? extends E
向下限定,E及其子类
? super E
向上限定,E及其父类
三种迭代的能否删除
- 普通for循环,可以删除,但是索引要(--减减 )
- 迭代器,可以删除,但是必须使用迭代器自身的remove方法,否则会出现并发修改异常
- 增强for循环不能删除
HashSet 的实现原理
HashSet 概述
HashSet 实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
HashSet 的实现
对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成, HashSet 的源代码如下
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; // 底层使用 HashMap 来保存 HashSet 中所有元素。 private transient HashMap<E, Object> map; // 定义一个虚拟的 Object 对象作为 HashMap 的 value,将此对象定义为 static final。 private static final Object PRESENT = new Object(); /** * 默认的无参构造器,构造一个空的 HashSet。 * * 实际底层会初始化一个空的 HashMap,并使用默认初始容量为 16 和加载因子 0.75。 */ public HashSet() { map = new HashMap<E, Object>(); } /** * 构造一个包含指定 collection 中的元素的新 set。 * * 实际底层使用默认的加载因子 0.75 和足以包含指定 collection 中所有元素的初始容量来创建一个 HashMap。 * * @param c * 其中的元素将存放在此 set 中的 collection。 */ public HashSet(Collection<? extends E> c) { map = new HashMap<E, Object>(Math.max((int) (c.size() / .75f) + 1, 16)); addAll(c); } /** * 以指定的 initialCapacity 和 loadFactor 构造一个空的 HashSet。 * * 实际底层以相应的参数构造一个空的 HashMap。 * * @param initialCapacity * 初始容量。 * @param loadFactor * 加载因子。 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E, Object>(initialCapacity, loadFactor); } /** * 以指定的 initialCapacity 构造一个空的 HashSet。 * * 实际底层以相应的参数及加载因子 loadFactor 为 0.75 构造一个空的 HashMap。 * * @param initialCapacity * 初始容量。 */ public HashSet(int initialCapacity) { map = new HashMap<E, Object>(initialCapacity); } /** * 以指定的 initialCapacity 和 loadFactor 构造一个新的空链接哈希集合。 此构造函数为包访问权限,不对外公开,实际只是是对 * LinkedHashSet 的支持。 * * 实际底层会以指定的参数构造一个空 LinkedHashMap 实例来实现。 * * @param initialCapacity * 初始容量。 * @param loadFactor * 加载因子。 * @param dummy * 标记。 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E, Object>(initialCapacity, loadFactor); } /** * 返回对此 set 中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 * * 底层实际调用底层 HashMap 的 keySet 来返回所有的 key。 可见 HashSet 中的元素,只是存放在了底层 HashMap 的 key * 上, value 使用一个 static final 的 Object 对象标识。 * * @return 对此 set 中元素进行迭代的 Iterator。 */ public Iterator<E> iterator() { return map.keySet().iterator(); } /** * 返回此 set 中的元素的数量(set 的容量)。 * * 底层实际调用 HashMap 的 size()方法返回 Entry 的数量,就得到该 Set 中元素的个数。 * * @return 此 set 中的元素的数量(set 的容量)。 */ public int size() { return map.size(); } /** * 如果此 set 不包含任何元素,则返回 true。 * * 底层实际调用 HashMap 的 isEmpty()判断该 HashSet 是否为空。 * * @return 如果此 set 不包含任何元素,则返回 true。 */ public boolean isEmpty() { return map.isEmpty(); } /** * 如果此 set 包含指定元素,则返回 true。 更确切地讲,当且仅当此 set 包含一个满足(o==null ? e==null : * o.equals(e)) 的 e 元素时,返回 true。 * * 底层实际调用 HashMap 的 containsKey 判断是否包含指定 key。 * * @param o * 在此 set 中的存在已得到测试的元素。 * @return 如果此 set 包含指定元素,则返回 true。 */ public boolean contains(Object o) { return map.containsKey(o); } /** * 如果此 set 中尚未包含指定元素,则添加指定元素。 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : * e.equals(e2)) 的元素 e2,则向此 set 添加指定的元素 e。 如果此 set 已包含该元素,则该调用不更改 set 并返回 false。 * * 底层实际将将该元素作为 key 放入 HashMap。 由于 HashMap 的 put()方法添加 key-value 对时,当新放入 HashMap * 的 Entry 中 key 与集合中原有 Entry 的 key 相同(hashCode()返回值相等,通过 equals 比较也返回 true), * 新添加的 Entry 的 value 会将覆盖原来 Entry 的 value,但 key 不会有任何改变, 因此如果向 HashSet * 中添加一个已经存在的元素时,新添加的集合元素将不会被放入 HashMap 中, 原来的元素也不会有任何改变,这也就满足了 Set 中元素不重复的特性。 * * @param e * 将添加到此 set 中的元素。 * @return 如果此 set 尚未包含指定元素,则返回 true。 */ public boolean add(E e) { return map.put(e, PRESENT) == null; } /** * 如果指定元素存在于此 set 中,则将其移除。 更确切地讲,如果此 set 包含一个满足(o==null ? e==null : * o.equals(e))的元素e, 则将其移除。如果此 set 已包含该元素,则返回 true (或者:如果此 set 因调用而发生更改,则返回 * true)。(一旦调用返回,则此 set 不再包含该元素)。 * * 底层实际调用 HashMap 的 remove 方法删除指定 Entry。 * * @param o * 如果存在于此 set 中则需要将其移除的对象。 * @return 如果 set 包含指定元素,则返回 true。 */ public boolean remove(Object o) { return map.remove(o) == PRESENT; } /** * 从此 set 中移除所有元素。此调用返回后,该 set 将为空。 * * 底层实际调用 HashMap 的 clear 方法清空 Entry 中所有元素。 */ public void clear() { map.clear(); } /** * 返回此 HashSet 实例的浅表副本:并没有复制这些元素本身。 * * 底层实际调用 HashMap 的 clone()方法,获取 HashMap 的浅表副本,并设置到 HashSe * */ public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } } }
HashSet存储字符串并遍历
Set集合概述及特点
- 通过API查看Set
- Set 是一个不包含重复元素的 collection
- Set只是一个接口,一般使用它的子类HashSet, LinkedHashSet, TreeSet
HaseSet
- 此类实现 Set 接口,由哈希表支持
- 它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。
- 此类允许使用 null 元素
HashSet存储字符串并遍历
- 使用增强for循环
- 迭代器
HashSet如何保证元素唯一性的原理
HashSet原理
- 使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数
- 当HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象
- 如果没有哈希值相同的对象就直接存入集合
- 如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存
将自定义类的对象存入HashSet去重复的关键
- 类中必须重写hashCode()和equals()方法
- hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)
- equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储
为什么自动生成hashcode的时有个31的数
- 31是一个质数,质数是能被1和自己本身整除的数,没有公约数
- 31这个数既不大也不小
- 大的话可能超过int的取值范围
- 小的话,相同率机出现比较多
- 31这个数好算,2的五次方-1,1向左移动5位 - 1, [1 << 5] -1
将List集合中的重复元素去掉(set实现)
LinkedHashSet的概述和使用
- LinkedHashSet是一个具有可预知迭代顺序的 Set 接口。
- 内部实现是使用哈希表和链接列表
- LinkedHashSet的特点是可以保证怎么存就怎么取
- LinkedHashSet是set集合中唯一一个能保证怎么存就怎么取的集合对象
- LinkedHashSet是HashSet的子类,所以也是保证元素唯一的,与HashSet的原理一样
TreeSet存储Integer类型的元素并遍历
- TreeSet是一个可以用于排序的集合
- TreeSet:基于 TreeMap 的 NavigableSet 实现。
- TreeSet的排序方法有两种
- 使用元素的自然顺序Comparable对元素进行排序
- 使用构造方法的 Comparator 进行排序
TreeSet存储自定义对象
使用TreeSet存储自定义对象时会出现异常:Person cannot be cast to java.lang.Comparable
- 如果想用TreeSet存储自定义对象,这个对象必须要实现Comparable接口
- 此接口强行对实现它的每个类的对象进行整体排序。
- 这种排序被称为自然排序,类的 compareTo 方法被称为它的自然比较方法。
- 当compareTo方法返回0的时候集合中只有一个元素
- 当compareTo方法返回正数的时候集合会怎么存就怎么取
- 当compareTo方法返回负数的时候集合会倒序存储
例子:TreeSet存储自定义对象并遍历
TreeSet存储自定义对象并遍历,按照姓名长度、字母、年龄排序
- 通过比较字符串的compareTo方法可以比较大小
- 排序是按照unicode码的大小进行排序的
- 防止名字相同,但年龄不同的bug
TreeSet的构造方法(比较器)
- TreeSet(Comparator<? super E> comparator)
- TreeSet有个带Comparator参数的构造方法
- 构造一个新的空 TreeSet,它根据指定的 “比较器”进行排序
TreeSet排序原理总结
TreeSet的特点
TreeSet是用来排序的, 可以指定一个顺序, 对象存入之后会按照指定的顺序排列
TreeSet排序方式有两种自然顺序和比较器顺序
- 自然顺序(Comparable)
- TreeSet类的add()方法中会把存入的对象提升为Comparable类型
- 调用对象的compareTo()方法和集合中的对象比较
- 根据compareTo()方法返回的结果进行存储
- 比较器顺序(Comparator)
- 创建TreeSet的时候可以制定 一个Comparator
- 如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的规则比较
- add()方法内部会自动调用Comparator接口中compare()方法排序
- 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
Map集合概述和特点
Map是属于java.util的一个接口Map<K,V>
类型参数:
- K - 映射所维护的键的类型
- V - 映射值的类型
Map是将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
Map接口和Collection接口的不同
- Map是双列的,Collection是单列的
- Map的键唯一,Collection的Set是唯一的
- Map集合的数据结构值针对键有效,跟值无关
- Collection集合的数据结构是针对元素有效
Map是一个接口,一般使用它的子类HashMap
HashMap 概述
HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 的数据结构
在 java 编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap 也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
从上图中可以看出,HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
可以看出,Entry 就是数组中的元素,每个 Map.Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。
HashMap 的存取实现
存储
public V put(K key, V value) { // HashMap 允许存放 null 键和 null 值。 // 当 key 为 null 时,调用 putForNullKey 方法,将 value 放置在数组第一个位置。 if (key == null) return putForNullKey(value); // 根据 key 的 hashCode 重新计算 hash 值。 int hash = hash(key.hashCode()); // 搜索指定 hash 值在对应 table 中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 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; } } // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry。 modCount++; // 将 key、value 添加到 i 索引处。 addEntry(hash, key, value, i); return null; }
从上面的源代码中可以看出:当往 HashMap 中 put 元素的时候,先根据 key 的hashCode 重新计算 hash 值,根据 hash 值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
addEntry(hash, key, value, i)方法根据计算出的 hash 值,将 key-value 对放在数组 table的 i 索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:
void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K, V> e = table[bucketIndex]; // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entr table[bucketIndex] = new Entry<K, V>(hash, key, value, e); // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) // 把 table 对象的长度扩充到原来的 2 倍。 resize(2 * table.length); }
当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
hash(int h)方法根据 key 的 hashCode 重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的 hash 冲突。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
可以看到在 HashMap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 HashMap 的数据结构是数组和链表的结合,所以我们当然希望这个 HashMap 里面的 元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。首先想到的就是把 hash 值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在 HashMap 中是这样做的:调用indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
static int indexFor(int h, int length) { return h & (length-1) }
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap底层数组的长度总是 2 的 n 次方,这是 HashMap 在速度上的优化。在 HashMap 构造器中有如下代码:
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
这段代码保证初始化时 HashMap 的容量总是 2 的 n 次方,即底层数组的长度总是为 2的 n 次方。当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是
h%length,但是&比%具有更高的效率。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
从上面的例子中可以看出:当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到 8 或者 9,这样就降低了查询的效率。同时,我们也可以发现,当数组
以上是关于Java 集合的主要内容,如果未能解决你的问题,请参考以下文章