一 容器概述

在Java内存层面上,对多个对象进行统一管理和操作的的容器有:集合,数组,哈希结构
1)数组在存储数据方面的特点

  1. 数组一旦初始化,它的长度就是确定的
  2. 数组有索引,可以很方便的对指定位置上的元素进行查找和替换
  3. 数组在存储数据的时候是有序的,且使用连续的内存空间
  4. 数组在定义的时候,就明确了存储的数据的类型

2)数组在存储数据方面的弊端

  1. 数组一旦初始化,它的长度就不能改变,如果需要扩容,则必须新建一个数组
  2. 数组在插入,删除操作的效率较差
  3. 可以表达的多个对象之间的关系较为简单,不能表示丰富的对象关系
  4. 数组中可使用的方法非常少

    1.1 数组和集合之间的转换

    1)将集合转为数组:toArray() ```java Collection c1 = new ArrayList(); c1.add(123);//自动装箱 c1.add(new Person(“Tom”,12)); c1.add(new String(“AA”)); c1.add(new Date(234234324L)); c1.add(“BB”);

Object[] arr = c1.toArray(); for(int i = 0;i < arr.length;i++){ System.out.println(arr[i]); }

  1. 2)将数组转化为集合:**Arrays.asList(T...t)**
  2. ```java
  3. String[] arr1 = {"AA","CC","MM","GG"};
  4. List list = Arrays.asList(arr1);
  5. System.out.println(list);
  6. List list1 = Arrays.asList("AA","CC","MM");

1.2 Java集合的两种体系

1.2.1 Collection

  • 单列数据,定义了存储一组对象的方法的集合

1)List:元素有序,可重复的集合
2)Set:元素无序,不可重复的集合

1.2.2 Map

1)双列数据,保存具有映射关系的 k-v 键值对的集合

二 Collection接口

2.1 Collection概述

1)Collection接口是ListSetQueue 接口的父接口。该接口中定义的方法可以用于其各种子接口的实现类中。
2)JDK不提供Collection接口的实现类,而是提供更具体的子接口来实现。
3)在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。

2.1.1 Collection接口定义的方法

  1. add(Object obj) // 添加元素obj到当前集合中
  2. size() // 获取集合中元素的个数
  3. addAll(Collection coll) // 将coll集合中的元素添加到当前集合中。
  4. clear() // 清空当前集合
  5. isEmpty() // 判断当前集合是否为空。
  6. contains(Object obj) // 判断当前集合中是否包含obj元素,具体的:需要调用obj对象所在类的equals(Object o)
  7. containsAll(Collection coll) // 判断当前集合中是否包含coll集合中的所有元素
  8. remove(Object obj) // 删除当前集合中首次出现的obj元素
  9. removeAll(Collection coll) //差集:从当前集合中移除其与coll集合共有的元素
  10. retainAll(Collection coll) // 交集:获取当期集合与coll集合共有
  11. equals(Object obj) // 判断当前集合与obj元素是否相等,要想返回true,要求形参obj也是一个同类型的集合对象,同时集合元素都相同。(如果是List的话,要求顺序也相同)
  12. hashCode() // 返回当前集合对象的哈希值

2.2.2 迭代器接口:Iterator

1)Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
2)提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
3)Collection继承了java.lang.Iterable 接口,该接口有一个 iterator() 方法,所以所有地 Collection 子接口实现类集合都有一个 iterator() 方法,用于返回一个实现了 Iterator接口的对象。
4)Iterator仅用于遍历集合,本身不具有承载对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
5)集合每一次调用 iterator() 方法都会得到一个全新的迭代器对象,默认游标在集合的第一个元素前。
Iterator接口的方法:

  1. hasNext() // 判断是否存在下一个元素
  2. E next() // 返回集合的一个元素,并将游标移向下一位
  3. remove() // 删除集合的一个元素(使用方式还不清晰)

2.2 List子接口

List概述:
①鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
②List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
③List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
④JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。

2.2.1 List接口新增的方法

  1. void add(int index, Object ele) // 在index位置插入ele元素
  2. boolean addAll(int index, Collection eles) // 从index位置开始将eles中的所有元素添加进来
  3. Object get(int index) // 获取指定index位置的元素
  4. int indexOf(Object obj) // 返回obj在集合中首次出现的位置
  5. int lastIndexOf(Object obj) // 返回obj在当前集合中末次出现的位置
  6. Object remove(int index) // 移除指定index位置的元素,并返回此元素
  7. remove(Object obj) // 移除集合的指定元素,但是如果传入的是int类型,那么会自动调用的是remove(int index)方法
  8. Object set(int index, Object ele) // 设置指定index位置的元素为ele
  9. List subList(int fromIndex, int toIndex) // 返回从fromIndex到toIndex位置的子集合

注意:remove(Object obj),remove(int index) 当传入一个int类型的元素时,调用的是第二个方法,但是如果想要int类型匹配的是Object元素的时候,那么就应该手动将int装箱。
如上述 list.remove(123) 报异常,数组越界,虽然list有123元素,但是默认调用的索引参数的方法。
list.remove(new Integer(123)) 删除的是集合中的元素 123。

  1. 增:add(Object obj)
  2. 删:remove(Object obj) / remove(int index)
  3. 改:set(int index, Object ele)
  4. 查:get(int index)
  5. 插:add(int index, Object ele)
  6. 长度:size()
  7. 遍历:iterator() / 增强for循环 / for

2.2.2 ArrayList

  1. // JDK8源码
  2. public class ArrayList<E> extends AbstractList<E>
  3. implements List<E>, RandomAccess, Cloneable, java.io.Serializable

1)ArrayList概述:
①是List接口的典型实现类,主要实现类。
②本质上,ArrayList是对象引用的一个“变长”的动态数组。
③在JDK7时,ArrayList更像饿汉式,直接创建一个初始容量为10的数组;在JDK8,变得像懒汉式,开始只创建一个长度为0的数组,当添加第一个元素时,再创建一个容量为10的数组。
④Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合。
⑤线程不安全,效率高,底层使用Object数组存储。

2.2.3 LinkedList

  1. public class LinkedList<E>
  2. extends AbstractSequentialList<E>
  3. implements List<E>, Deque<E>, Cloneable, java.io.Serializable

1)LinkedList概述:
①底层使用双向链表来存储数据,对于频繁的插入,删除操作的时候效率高
②内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:

  1. // prev变量记录前一个元素的位置
  2. // next变量记录下一个元素的位置
  3. private static class Node<E> {
  4. E item;
  5. Node<E> next;
  6. Node<E> prev;
  7. Node(Node<E> prev, E element, Node<E> next) {
  8. this.item = element;
  9. this.next = next;
  10. this.prev = prev;
  11. }
  12. }

2)LinkedList类新增的方法:

  1. void addFirst(Object obj)
  2. void addLast(Object obj)
  3. Object getFirst()
  4. Object getLast()
  5. Object removeFirst()
  6. Object removeLast()

2.2.4 Vector

  1. public class Vector<E>
  2. extends AbstractList<E>
  3. implements List<E>, RandomAccess, Cloneable, java.io.Serializable

1)Vector概述:
①古老的实现类,在jdk1.0的时候就存在了,线程安全,效率低,底层也是使用Object数组来存储的。
②大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
2)Vector类新增的方法:

  1. void addElement(Object obj)
  2. void insertElementAt(Object obj,int index)
  3. void setElementAt(Object obj,int index)
  4. void removeElement(Object obj)
  5. void removeAllElements()

2.3 Set子接口

Set概述:
①Set接口是Collection的子接口,set接口没有提供额外的方法。
②Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
③Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。

2.3.1 HashSet

1)HashSet概述:
①是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类
②按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
③不能保证元素的排列顺序,不是线程安全的,集合元素可以为null
④对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
2)HashSet判断两个元素相同的标准:两个对象通过hashcode()方法比较哈希值相等,并且使用两个对象的equals()方法比较返回true。

2.3.2 LinkedHashSet

  1. public class LinkedHashSet<E>
  2. extends HashSet<E>
  3. implements Set<E>, Cloneable, java.io.Serializable {

LinkedHashSet概述:
①LinkedHashSet 是 HashSet 的子类。
②LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
③LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
④LinkedHashSet 不允许集合元素重复。

2.3.3 TreeSet

  1. public class TreeSet<E> extends AbstractSet<E>
  2. implements NavigableSet<E>, Cloneable, java.io.Serializable

1)TreeSet概述:
①TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
②TreeSet底层使用红黑树结构存储数据。
③TreeSet和 TreeMap采用红黑树的存储结构。
④特点:有序,查询速度比List快。
2)TreeSet新增的方法:

  1. Comparator comparator()
  2. Object first()
  3. Object last()
  4. Object lower(Object e)
  5. Object higher(Object e)
  6. SortedSet subSet(fromElement, toElement)
  7. SortedSet headSet(toElement)
  8. SortedSet tailSet(fromElement)

3)TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。

三 Map接口

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 接口使用频率最高的实现类。

3.1 常用方法

3.1.1 添加,删除,修改

  1. Object put(Object key, Object value):将指定key-value添加到(或修改)当前map对象中
  2. void putAll(Map m):将m中的所有key-value对存放到当前map中
  3. Object remove(Object key):移除指定key的key-value对,并返回value
  4. void clear():清空当前map中的所有数据

3.1.2 查询获取

  1. Object get(Object key):获取指定key对应的value
  2. boolean containsKey(Object key):是否包含指定的key
  3. boolean containsValue(Object value):是否包含指定的value
  4. int size():返回map中key-value对的个数
  5. boolean isEmpty():判断当前map是否为空
  6. boolean equals(Object obj):判断当前map和参数对象obj是否相等

3.1.3 元视图操作

  1. Set keySet():返回所有key构成的Set集合
  2. Collection values():返回所有value构成的Collection集合
  3. Set entrySet():返回所有key-value对构成的Set集合
  • 例子
    1. Map map = new HashMap();
    2. // map.put(..,..)省略
    3. System.out.println("map的所有key:");
    4. Set keys = map.keySet(); // HashSet
    5. for (Object key : keys) {
    6. System.out.println(key + "->" + map.get(key));
    7. }
    8. System.out.println("map的所有的value:");
    9. Collection values = map.values();
    10. Iterator iter = values.iterator();
    11. while (iter.hasNext()) {
    12. System.out.println(iter.next());
    13. }
    14. System.out.println("map所有的映射关系:");
    15. // 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
    16. Set mappings = map.entrySet();
    17. for (Object mapping : mappings) {
    18. Map.Entry entry = (Map.Entry) mapping;
    19. System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
    20. }

    3.2 HashMap

    HashMap最早出现在 JDK1.2 中,底层基于散列算法实现的。HashMap允许 null 作为 键,值。当 null 作为 键时,哈希值为 0。HashMap 不保证键值对的顺序,这就意味着在进行某些操作后,键值对的顺序可能会发生变化。HashMap 同时也是线程不安全类,在并发环境下会有安全问题。
    (这里主要研究 JDK1.8 的源码)

HashMap 经过几代的优化,现在已经变得很复杂了。涉及的知识点也很多。主要包括:

  1. 散列表的实现
  2. 扰动函数
  3. 初始化容量
  4. 负载因子
  5. 扩容元素拆分
  6. 链表树化
  7. 红黑树
  8. 插入
  9. 查找
  10. 删除
  11. 遍历
  12. 分段锁

    3.2.1 散列表的实现

    1、当有一串元素,需要存放在一个数组中,要求在获取每个元素的时候,时间复杂度是:O(1)。也就是说存放在数组的元素,在获取的时候,需要直接通过下标来获取到指定的元素,不能使用遍历获取的方式。
    2、那么每个元素存储在数组中的位置,可以使用元素的 HashCode 来作为数组的下标,这样在想要获取特定的元素的时候,可以通过元素的 hashCode 来获取。
    3、但是需要注意,hashCode的范围为 [-2147483648, 2147483647],数组的长度不能为这么长,可以让 hashCode 和数组元素做与运算,得到一个存放在数组的下标位置。如果出现有两个相同的下标,则可以使用链表,红黑树的方式在存放相同下标的元素。

HashMap数组的长度为啥需要为 2 的幂次方?
在对哈希值映射到数组的下标时,一般是使用hashCode直接对数组长度进行求余【hash % n】,Java8中的源码优化是使用了与运算,计算速度更快,但是这个前提就是数组的长度 n 需要是 2 的倍数,在这个前提下,【hash % n】才等价于【(n-1) & hash】。所以在源码的 get,put 方法中,都可以看到 tab[ (n-1) & hash ],就是给数组的下标位置存储元素。

3.2.2 扰动函数

Java8中 HashMap 的扰动函数

  1. static final int hash(Object key) {
  2. int h;
  3. // 如果 key 为 null,那么key 的哈希值就是 0
  4. // 将 key 的哈希值右移 16 位,然后和原哈希值进行异或运算,
  5. // 这样可以混合了原哈希值的高位和低位,增大了随机性
  6. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  7. }

总结:扰动函数的存在就是为了增加随机性,让数据元素更加均匀的散列,减少碰撞

3.2.3 初始化容量和负载因子

HashMap有一个构造器,可以在创建对象的时候传入初始容量。但是在此构造器中,会通过 tableSizeFor 这个方法计算出一个比该值大的最小的 2 的幂次方。如:传入一个 17,那么比它大的最小的 2 的幂次方就是 32,那么阈值就是 32。即 threshold = 32.

HashMap 还有一个 loadFactor 属性:负载因子,默认值为 0.75. 这个值存在的意义就是当 hashMap 中实际存储的元素数量大于 n*0.75 时,就需要进行扩容了。

3.2.4 扩容元素拆分

一般扩容后的数组长度为原数组的2倍。这时不需要对所有的元素进行重新计算哈希值,只需要将哈希值和扩容新增的长度进行 & 运算,如果为 0 则下标不变,如果不为 0,则需要将位置+新增的数组长度,这个就是其扩容后元素的存储位置。

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4. int oldThr = threshold;
  5. int newCap, newThr = 0;
  6. // 这里的if-else主要计算扩容后的元素数组的长度和阈值是多少
  7. if (oldCap > 0) {
  8. // 1、旧的数组长度大于0,证明hashMap已经初始化过
  9. if (oldCap >= MAXIMUM_CAPACITY) {
  10. // 如果数组长度已大于 1<<30,那么阈值调整为整数的最大值
  11. threshold = Integer.MAX_VALUE;
  12. // 不进行扩容,返回原数组
  13. return oldTab;
  14. }
  15. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  16. oldCap >= DEFAULT_INITIAL_CAPACITY)
  17. newThr = oldThr << 1; // double threshold
  18. }
  19. else if (oldThr > 0) // initial capacity was placed in threshold
  20. newCap = oldThr;
  21. else { // zero initial threshold signifies using defaults
  22. newCap = DEFAULT_INITIAL_CAPACITY; // 16
  23. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12=16*0.75
  24. }
  25. if (newThr == 0) {
  26. float ft = (float)newCap * loadFactor;
  27. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  28. (int)ft : Integer.MAX_VALUE);
  29. }
  30. threshold = newThr;
  31. @SuppressWarnings({"rawtypes","unchecked"})
  32. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  33. table = newTab;
  34. if (oldTab != null) {
  35. for (int j = 0; j < oldCap; ++j) {
  36. Node<K,V> e;
  37. if ((e = oldTab[j]) != null) {
  38. oldTab[j] = null;
  39. if (e.next == null)
  40. newTab[e.hash & (newCap - 1)] = e;
  41. else if (e instanceof TreeNode)
  42. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  43. else { // preserve order
  44. Node<K,V> loHead = null, loTail = null;
  45. Node<K,V> hiHead = null, hiTail = null;
  46. Node<K,V> next;
  47. do {
  48. next = e.next;
  49. if ((e.hash & oldCap) == 0) {
  50. if (loTail == null)
  51. loHead = e;
  52. else
  53. loTail.next = e;
  54. loTail = e;
  55. }
  56. else {
  57. if (hiTail == null)
  58. hiHead = e;
  59. else
  60. hiTail.next = e;
  61. hiTail = e;
  62. }
  63. } while ((e = next) != null);
  64. if (loTail != null) {
  65. loTail.next = null;
  66. newTab[j] = loHead;
  67. }
  68. if (hiTail != null) {
  69. hiTail.next = null;
  70. newTab[j + oldCap] = hiHead;
  71. }
  72. }
  73. }
  74. }
  75. }
  76. return newTab;
  77. }

3.2.5 插入元素

  1. /**
  2. * 插入一个元素
  3. * @param key 键
  4. * @param value 值
  5. */
  6. public V put(K key, V value) {
  7. // 首先对key进行hash值的扰动,获取一个新的哈希值
  8. return putVal(hash(key), key, value, false, true);
  9. }
  1. /**
  2. * 这个方法被 put 方法调用,是它和相关方法的实现
  3. *
  4. * @param hash 键的哈希值
  5. * @param key 键
  6. * @param value 值
  7. * @param onlyIfAbsent 如果为true,那么就不改变现有值,即不做更新
  8. * @param evict 如果为 false,则表示处于创建模式
  9. * @return 前一个value值,如果没有,则返回null
  10. */
  11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  12. boolean evict) {
  13. Node<K,V>[] tab;
  14. Node<K,V> p;
  15. int n, i;
  16. // 判断hashMap的数组table是为null,或者长度是否为0,如果是,那么先进行扩容
  17. if ((tab = table) == null || (n = tab.length) == 0)
  18. n = (tab = resize()).length;
  19. // i = (n - 1) & hash 根据哈希值计算数组的下标,如果对应的下标位置数组中没有元素,则插入
  20. if ((p = tab[i = (n - 1) & hash]) == null)
  21. tab[i] = newNode(hash, key, value, null);
  22. else {
  23. // 否则
  24. Node<K,V> e; K k;
  25. if (p.hash == hash &&
  26. ((k = p.key) == key || (key != null && key.equals(k))))
  27. e = p;
  28. else if (p instanceof TreeNode)
  29. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  30. else {
  31. for (int binCount = 0; ; ++binCount) {
  32. if ((e = p.next) == null) {
  33. p.next = newNode(hash, key, value, null);
  34. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  35. treeifyBin(tab, hash);
  36. break;
  37. }
  38. if (e.hash == hash &&
  39. ((k = e.key) == key || (key != null && key.equals(k))))
  40. break;
  41. p = e;
  42. }
  43. }
  44. if (e != null) { // existing mapping for key
  45. V oldValue = e.value;
  46. if (!onlyIfAbsent || oldValue == null)
  47. e.value = value;
  48. afterNodeAccess(e);
  49. return oldValue;
  50. }
  51. }
  52. ++modCount;
  53. if (++size > threshold)
  54. resize();
  55. afterNodeInsertion(evict);
  56. return null;
  57. }

3.2.1 存储结构

JDK7及以前:HashMap是数组+链表结构(链地址法) JDK8及以后:HashMap是数组+链表+红黑树

3.2.1.1 JDK7中的HashMap

1)HashMap在JDK7中的存储结构
HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

2)添加元素的过程
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值相同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

3)HasnMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

4)那么HashMap在什么时候扩容合适
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组元素存在的个数size)loadFactor(负载因子) 时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

3.2.1.2 JDK8中的HashMap

1)HashMap在JDK8中的存储结构
HashMap的内部存储结构其实是数组+链表+红黑树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

2)HashMap进行扩容和树形化
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

3)关于映射关系的key是否可以修改?

答案:不要修改

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

4)总结:JDK1.8相较之前的变化
1)HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
2)当首次调用map.put()时,再创建长度为16的数组
3)数组为Node类型,在jdk7中称为Entry类型
4)形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
5)当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

3.3 LinkedHashMap

1)概述:LinkedHashMap 是 HashMap 的子类。在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。

2)HashMap的内部类:Node

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. V value;
  5. Node<K,V> next;
  6. }

3)LinkedHashMap的内部类:Entry

  1. static class Entry<K,V> extends HashMap.Node<K,V> {
  2. Entry<K,V> before, after;
  3. Entry(int hash, K key, V value, Node<K,V> next) {
  4. super(hash, key, value, next);
  5. }
  6. }

3.4 TreeMap

1)TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
2)TreeSet底层使用红黑树结构存储数据。
3)TreeMap 的 Key 的排序:

  • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。
  • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key Comparable 接口。

4)TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

3.5 Hashtable

1)Hashtable是个古老的 Map 实现类,JDK1.0就存在了。不同于HashMap,Hashtable是线程安全的。
2)Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
3)与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value。
4)与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序。
5)Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

3.6 Properties

1)Properties 类是 Hashtable 的子类,该对象用于处理属性文件。
2)由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型。
3)存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

  1. Properties pros = new Properties();
  2. pros.load(new FileInputStream("jdbc.properties"));
  3. String user = pros.getProperty("user");
  4. System.out.println(user);

四 Collections工具类

4.1 概述

1)Collections 是一个操作 Set、List 和 Map 等集合的工具类
2)Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
排序操作:(均为static方法)

  1. reverse(List):反转List 中元素的顺序
  2. shuffle(List):对List集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
  4. sort(ListComparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  5. swap(Listint int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

4.2 Collections常用方法

1)查找、替换

  1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  2. Object max(CollectionComparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  3. Object min(Collection)
  4. Object min(CollectionComparator)
  5. int frequency(CollectionObject):返回指定集合中指定元素的出现次数
  6. void copy(List dest,List src):将src中的内容复制到dest
  7. boolean replaceAll(List listObject oldValObject newVal):使用新值替换 List 对象的所有旧值

2)同步控制:Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

五 常见面试题

1、jdk8后,HashMap极限情况下,存储多少条数据会形成红黑二叉树?
答:极限情况下,存储11条数据会形成红黑树
①链表的元素为8个:当HashMap的链表的数据超过8个的时候,会尝试转换为二叉树
②链表的元素为9个:判断容量是否足够,扩容 162=>32
③链表的元素为10个:判断容量是否足够,扩容 32
2=>64
④链表元素为11个:转换为红黑二叉树

2、为什么HashMap扩容必须是2倍
答:必须是2的n次方,才有可能让数组中的位置元素都有可能倍占用,否则就会浪费内存了

3、集合类在遍历的时候同时删除某个元素就会报异常:

  1. Java ConcurrentModificationException