Java集合

Java集合

集合框架

  • Java集合主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)

主要接口概述

Collection接口
  • 是List、Set和Queue接口的父接口,定义了可用于操作List、Set和Queue的方法-增删改查
  • 定义的主要方法:
List接口
  • List是元素有序并且可以重复的集合,被称为序列,可以精确的控制每个元素的插入位置,或删除某个位置元素
  • List接口的常用子类:
    • ArrayList
    • LinkedList
    • Vector
    • Stack
  • 下图是List的JDK源码UML图
Set接口
  • Set接口中不能加入重复元素,无序
  • Set接口常用子类:
    • 散列存放:HashSet
    • 有序存放:TreeSet
  • 下图是Set的JDK源码UML图
Map和HashMap
  • Map提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据key快速查找value
    • Map中的键值对以Entry类型的对象实例形式存在
    • 键(key值)不可重复,value值可以
    • 每个建最多只能映射到一个值
    • Map接口提供了分别返回key值集合、value值集合以及Entry(键值对)集合的方法
    • Map支持泛型,形式如:Map<K,V>
  • HashMap是Map的一个重要实现类,也是最常用,基于哈希表实现
    • HashMap中的Entry对象是无序排列的
    • Key值和Value值都可以为null,但是一个HashMap只能有一个key值为null的映射(key值不可重复)
  • 下图是Map的JDK源码UML图
Comparable和Comparator
  • Comparable接口——可比较的
    • 实现该接口表示:这个类的实例可以比较大小,可以进行自然排序
    • 定义了默认的比较规则
    • 其实现类需要实现compareTo()方法
    • compareTo()方法返回正数表示大,负数表示小0表示相等
  • Comparator接口——比较工具接口
    • 用于定义临时比较规则,而不是默认比较规则
    • 其实现类需要实现compare()方法
    • Comparable和Comparator都是Java集合框架的成员
Iterator接口
  • 集合输出的标准操作
    • 标准做法,使用Iterator接口
    • 操作原理:Iterator是专门的迭代输出接口,迭代输出就是将元素一个个进行判断,判断其是否有内容,如果有内容则把内容取出。

集合框架概述

  1. List的实现类主要有: LinkedList, ArrayList, Vector, Stack。
    • (01) LinkedList是双向链表实现的双端队列;不是线程安全的,只适用于单线程。
    • (02) ArrayList是数组实现的队列,是一个动态数组;不是线程安全的,只适用于单线程。
    • (03) Vector是数组实现的矢量队列,它也一个动态数组;不过和ArrayList不同的是,Vector是线程安全的,它支持并发。
    • (04) Stack是Vector实现的栈;和Vector一样,它也是线程安全的。
  2. Set的实现类主要有: HastSet和TreeSet。
    • (01) HashSet是一个没有重复元素的集合,它通过HashMap实现的;HashSet不是线程安全的,只适用于单线程。
    • (02) TreeSet也是一个没有重复元素的集合,不过和HashSet不同的是,TreeSet中的元素是有序的;它是通过TreeMap实现的;TreeSet也不是线程安全的,只适用于单线程。
  3. Map的实现类主要有: HashMap,WeakHashMap,LinkedHashmap, Hashtable和TreeMap。
    • (01) HashMap是存储“键-值对”的哈希表;它不是线程安全的,只适用于单线程。
    • (02) WeakHashMap是也是哈希表;和HashMap不同的是,HashMap的“键”是强引用类型,而WeakHashMap的“键”是弱引用类型,也就是说当WeakHashMap 中的某个键不再正常使用时,会被从WeakHashMap中被自动移除。WeakHashMap也不是线程安全的,只适用于单线程。
    • (03) Hashtable也是哈希表;和HashMap不同的是,Hashtable是线程安全的,支持并发。
    • (04) TreeMap也是哈希表,不过TreeMap中的“键-值对”是有序的,它是通过R-B Tree(红黑树)实现的;TreeMap不是线程安全的,只适用于单线程。
  • 框架中除了Vector、Hashtable、Statck、Enumeration其余都是非线程安全的。

各集合类分析

工具类Collections与Arrays
  • Collections主要方法:

    • public static <T> int binarySearch(List<? extends Comparable<? super T>> list,T key)使用二分搜索法搜索指定列表,以获得指定对象。在进行此调用之前,必须根据列表元素的自然顺序对列表进行升序排序(通过 sort(List) 方法)。如果没有对列表进行排序,则结果是不确定的。如果列表包含多个等于指定对象的元素,则无法保证找到的是哪一个。
    • public static <T> void copy(List<? super T> dest, List<? extends T> src)将所有元素从一个列表复制到另一个列表。执行此操作后,目标列表中每个已复制元素的索引将等同于源列表中该元素的索引(浅复制)。目标列表的长度至少必须等于源列表。如果目标列表更长一些,也不会影响目标列表中的其余元素。此方法以线性时间运行。
    • public static <T> Enumeration<T> enumeration(Collection<T> c)返回一个指定 collection 上的枚举。
    • public static int frequency(Collection<?> c, Object o)返回指定 collection 中等于指定对象的元素数。更确切地讲,返回 collection 中满足 (o == null ? e == null : o.equals(e)) 的 e 元素的数量。
    • public static int indexOfSubList(List<?> source, List<?> target)返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回 -1。此实现使用 “brute force” 扫描技术在源列表上进行扫描,依次在每个位置上寻找与目标匹配的列表项。返回最后一次出现的位置public static int lastIndexOfSubList(List<?> source, List<?> target)
    • public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)根据元素的自然顺序,返回给定 collection 的最大元素。collection 中的所有元素都必须实现 Comparable 接口。此外,collection 中的所有元素都必须是可相互比较的(也就是说,对于 collection 中的任意 e1 和 e2 元素,e1.compareTo(e2) 不得抛出 ClassCastException)此方法在整个 collection 上进行迭代,所以它需要的时间与 collection 的大小成正比。也可以执行比较器。min方法同理。
    • public static void reverse(List<?> list)反转指定列表中元素的顺序。此方法以线性时间运行。
    • public static <T extends Comparable<? super T>> void sort(List<T> list)根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。此外,列表中的所有元素都必须是可相互比较的(也就是说,对于列表中的任何 e1 和 e2 元素,e1.compareTo(e2) 不得抛出 ClassCastException)。此排序方法具有稳定性:不会因调用 sort 方法而对相等的元素进行重新排序。指定列表必须是可修改的,但不必是大小可调整的。该排序算法是一个经过修改的合并排序算法(其中,如果低子列表中的最高元素小于高子列表中的最低元素,则忽略合并)此算法提供可保证的 n log(n) 性能此实现将指定列表转储到一个数组中,并对数组进行排序,在重置数组中相应位置处每个元素的列表上进行迭代。只能排序实现list接口的类,可以指定比较器
    • public static void swap(List<?> list, int i, int j) 在指定列表的指定位置处交换元素。(如果指定位置相同,则调用此方法不会更改列表。)
  • Arrays主要方法:

    • Arrays的排序方法针对每种基本类型都做了实现,实现的方式有稍微的差异,但是思路都是相同的
      • 整个实现中的思路是 首先检查数组的长度,比一个阈值(286)小的时候直接使用双轴快排(DualPivotQuicksort)。其它情况下,先检查数组中数据的顺序连续性。把数组中连续升序或者连续降序的信息记录下来,顺便把连续降序的部分倒置。这样数据就被切割成一段段连续升序的数列。
      • 如果顺序连续性好,直接使用TimSort算法。TimSort算法的核心在于利用数列中的原始顺序,所以可以提高很多效率。
      • 顺序连续性不好的数组直接使用了 双轴快排 + 成对插入排序。成对插入排序是插入排序的改进版,它采用了同时插入两个元素的方式调高效率。双轴快排是从传统的单轴快排到3-way快排演化过来的
    • public static <T> List<T> asList(T... a) 返回一个受指定数组支持的固定大小的ArrayList。(对返回列表的更改会“直接写”到数组。)此方法同 Collection.toArray() 一起,充当了基于数组的 API 与基于 collection 的 API 之间的桥梁。返回的列表是可序列化的,并且实现了 RandomAccess。注意:此方法不支持直接传入基本类型的数组,否则会当做一个元素来处理。
    • public static int binarySearch(T[] a, T key) 用二分搜索法查询某个值,前提是数组必须是已排序的
    • public static boolean equals(Object[] a, Object[] a2)如果两个指定的 Objects 数组彼此相等,则返回 true。如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。此外,如果两个数组引用都为 null,则认为它们是相等的
    • public static void sort(int[] a)对指定的 int 型数组按数字升序进行排序。该排序算法是一个经过调优的快速排序法,也可以对数组中的指定段元素进行排序。
    • public static T[] copyOf()的实现是用的是System.arrayCopy(); copyOf()在内部新建一个数组,调用arrayCopy()将original内容复制到copy中去,并且长度为newLength。返回copy;一般情况下拷贝数组直接使用System的arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
    DualPivotQuicksort
    • DualPivotQuicksort是JDK1.7开始的采用的快速排序算法。中文名称:双支点快速排序。
    • 一般的快速排序采用一个枢轴来把一个数组划分成两半,然后递归之。大量经验数据表面,采用两个枢轴来划分成3份的算法更高效,这就是DualPivotQuicksort。
    • 具体步骤:选出两个枢轴P1和P2,需要3个指针L,K,G。3个指针的作用如下图:
      1. 小于27的数组,使用插入排序(或47)。
      2. 选择枢轴P1和P2。(假设使用数组头和尾)。
      3. P1需要小于P2,否者交换。现在数组被分成4份,left到L的小于P1的数,L到K的大于P1小于P2的数,G到rigth的大于P2的数,待处理的K到G的中间的数(逐步被处理到前3个区域中)。
      4. L从开始初始化直至不小于P1,K初始化为L-1,G从结尾初始化直至不大于P2。K是主移动的指针,来一步一步吞噬中间区域。
        • ****当大于P1小于P2,K++。
        • ****当小于P1,交换L和K的数,L++,K++。
        • ****当大于P2,如果G的数小于P1,把L上的数放在K上,把G的数放在L上,L++,再把K以前的数放在G上,G–,K++,完成一次L,K,G的互相交换。否则交换K和G,并G–,K++。
      5. 递归4。
      6. 交换P1到L-1上。交换P2到G+1上。
      7. 递归之。

Vector(同步)

  • Vector中每个向量会试图通过维护 capacity 和 capacityIncrement 来优化存储管理。capacity 始终至少应与向量的大小相等;这个值通常比后者大些,因为随着将组件添加到向量中,其存储将按 capacityIncrement 的大小增加存储块。应用程序可以在插入大量组件前增加向量的容量;这样就减少了增加的重分配的量。Vector是线程同步的。

  • Vector和ArrayList几乎是完全相同的,内部是通过数组实现的,它允许对元素进行快速随机访问。唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。

  • B:Vector类特有功能

    • public void addElement(E obj)
    • public E elementAt(int index)
    • public Enumeration elements()
    • Vector的迭代
    Vector v = new Vector();				//创建集合对象,List的子类
    v.addElement("a");
    v.addElement("b");
    //Vector迭代
    Enumeration en = v.elements();			//获取枚举
    while(en.hasMoreElements()) {			//判断集合中是否有元素
    	System.out.println(en.nextElement());//获取集合中的元素
    }
    
Fail-Fast机制
  • fail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。如HashMap、HashSet、ArrayList、LinkedList等不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map,那么将抛出 ConcurrentModificationException,
  • 在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
  • fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,因为 JDK 并不保证 fail-fast 机制一定会发生。若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
  • 原理:
    • 迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
    • expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:
    • ArrayList等非线程安全的集合中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。
HashMap(非同步)

Alt text

  • HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
  • HashMap解决哈希冲突的方法是开放地址和链地址法。
    • 开放地址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新节点存入该地址单元)。查找时探查到开放的地址表明表中无待查的关键字,即查找失败。
    • 链地址法:将所有的关键字为同义词的结点链接在同一单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0…m-1].凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。
  • HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • HashMap 的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
  • 如果很多映射关系要存储在HashMap实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
  • HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,再根据 equals 方法决定其在该数组位置上的链表中的存储位置,如果和链表中已有Entry与之相同则覆盖原先的value并返回此value,如果链表中不存在与之相同的Entry则将此键值对放到链表的链头,在Java8中,如果一个bucket中碰撞的元素超过某个限制(默认8个),则使用红黑树来替换链表,从而提高速度;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。
  • 当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,数组扩容这个操作也会出现在 ArrayList 中,这是一个常用的操作,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。
  • 当 HashMap 中的元素个数超过数组大小 * loadFactor 时,就会进行数组扩容,loadFactor的默认值为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过12的时候,就把数组的大小扩展为 2* 16=32 ,即扩大一倍,然后重新计 算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。
Hashtable(同步)
  • Hash表采用一个映射函数f:key --> address将关键字key映射到该记录在表中的存储位置。查询时通过键直接获取到该记录在表中的位置,复杂度为O(1)
  • Hashtable 继承于 Dictionary 类,实现了 Map, Cloneable, java.io.Serializable接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类,每个键和值都是对象。Hashtable线程安全的。
  • Hashtable实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。如果很多条目要存储在一个 Hashtable 中,那么与根据需要执行自动 rehashing 操作来增大表的容量的做法相比,使用足够大的初始容量创建哈希表或许可以更有效地插入条目。
LinkedHashMap(有序)
  • 事实上LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。上图给出了LinkedHashMap的结构图,主体部分跟HashMap完全一样,多了header指向双向链表的头部(是一个哑元),该双向链表的迭代顺序就是entry的插入顺序。

  • 除了可以保迭代历顺序,这种结构还有一个好处:迭代LinkedHashMap时不需要像HashMap那样遍历整个table,而只需要直接遍历header指向的双向链表即可,也就是说LinkedHashMap的迭代时间就只跟entry的个数相关,而跟table的大小无关

  • LinkedHashMap不仅像HashMap那样对其进行基于哈希表和单链表的Entry数组+ next链表的存储方式,而且还结合了LinkedList的优点,为每个Entry节点增加了前驱和后继,并增加了一个为header头结点,构造了一个双向循环链表。也就是说,每次put进来KV,除了将其保存到对哈希表中的对应位置外,还要将其插入到双向循环链表的尾部。当LinkedHashMap处于访问顺序遍历模式下,当执行get() 操作时,会将对应的Entry<k,v>移到遍历的最后位置。

  • 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序(创建LinkedHashMap对象时传入true),那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按 访问顺序排序的链表。如果需要按照访问顺序来排列的话在构造时将accessOrder传入true。

  • LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序

  • **protected boolean removeEldestEntry(Map.Entry<K,V> eldest) 当有新元素加入Map的时候会调用Entry的addEntry方法,会调用removeEldestEntry方法,这里就是实现LRU元素过期机制的地方,默认的情况下removeEldestEntry方法只返回false表示元素永远不过期。
    **,如果需要做到LRU需要创建一个新类继承LinkedHashMap并 override 这样一个方法,使得当缓存里存放的数据个数超过规定个数后,就把最不常用的移除掉。

     private static final int MAX_ENTRIES = 100;
     protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
     }
    
  • LinkedHashMap的属性比HashMap多了一个accessOrder属性。当它false时,表示双向链表中的元素按照Entry插入LinkedHashMap中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序;当它为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同,覆盖原有的Entry的情况下调用recordAccess方法),该方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用creatEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了),否则,什么也不做。

Hashset(非同步)
  • 当我们提到 HashSet 时,第一件事情就是在将对象存储在 HashSet 之前,要先确保对象重写 equals()和 hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
  • 对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成。
LinkedHashSet(有序)
  • LinkedHashSet 是 Set 的一个具体实现,其维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。与 HashMap 和 LinkedHashMap 的关系很像。
  • LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。
  • 如果我们需要迭代的顺序为插入顺序或者访问顺序,那么 LinkedHashSet 是需要你首先考虑的。
ArrayList(非同步)
  • ArrayList 是 List 接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是非同步的。)
  • 未指定初始容量时,默认初始容量为10,随着向 ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。
  • ArrayList 也采用了快速失败的机制,通过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
LinkedList(非同步)
  • LinkedList 继承自 AbstractSequenceList,实现了 List、Deque、Cloneable、java.io.Serializable 接 口。AbstractSequenceList 提供了List接口骨干性的实现以减少实现 List 接口的复杂度,Deque 接口定义了双端队列的操作。插入和删除操作比 ArrayList 更加高效。但也是由于其为基于链表的,所以随机访问的效率要比 ArrayList 差。
  • 在 LinkedList 中除了本身自己的方法外,还提供了一些可以使其作为栈、队列或者双端队列的方法。这些方法可能彼此之间只是名字不同,以使得这些名字在特定的环境中显得更加合适。
TreeSet
  • TreeSet是一个有序的集合,它的作用是提供有序的Set集合。底层数据结构是红黑树,排序效率高效。它继承了AbstractSet抽象类,实现了NavigableSet,Cloneable,Serializable接口。TreeSet是基于TreeMap实现的。
  • 排序
    • a.自然顺序(Comparable)
      • TreeSet类的add()方法中会把存入的对象提升为Comparable类型
      • 调用对象的compareTo()方法和集合中的对象比较
      • 根据compareTo()方法返回的结果进行存储
    • b.比较器顺序(Comparator)
      • 创建TreeSet的时候可以制定一个Comparator
      • 如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
      • add()方法内部会自动调用Comparator接口中compare()方法排序
      • 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
    • c.两种方式的区别
      • TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)
      • TreeSet如果传入Comparator, 就优先按照Comparator
TreeMap
  • 基于红黑树实现,映射根据其key的自然顺序进行排序,或根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法
  • TreeMap只能依据key来排序,不能根据value排序,如果想对value排序,可以将map集合的EntrySet转换成list,然后使用Collections.sort排序。**对value排序重要!!?*对于list的排序使用Collections工具类中的sort(List list,Comparator<? super T> c)注意只针对实现了list接口的类。
public static Map sortTreeMapByValue(Map map){
    ArrayList<Map.Entry> list = new ArrayList<>(map.entrySet());
    Collections.sort(list, new Comparator<Map.Entry>() {
        //升序排
        @Override
        public int compare(Map.Entry o1, Map.Entry o2) {
            return o1.getValue().toString().compareTo(o2.getValue().toString());
        }
    });

    for (Map.Entry<String, String> e: list) {
        System.out.println(e.getKey()+":"+e.getValue());
    }
    return map;
}
WeakHashMap(非同步)
  • 引用分类
    • 强引用:StrongReference,引用指向对象,gc(Garbage Collection)运行时不会回收
    • 软引用:SoftReference,gc运行时可能回收(jvm内存不够时)软引用可用于制作缓存
    • 弱引用:WeakReference,gc运行时立即回收
    • 虚引用:PhantomReference,类似于不引用,主要跟踪对象被回收的状态,不能单独使用,必须与引用队列(ReferenceQueue)联合使用
  • 以弱键实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除
  • WeakHashMap 类的行为部分取决于垃圾回收器的动作。因为垃圾回收器在任何时候都可能丢弃键,WeakHashMap 就像是一个被悄悄移除条目的未知线程。特别地,即使对 WeakHashMap 实例进行同步,并且没有调用任何赋值方法,在一段时间后 size 方法也可能返回较小的值,对于 isEmpty 方法,返回 false,然后返回true,对于给定的键,containsKey 方法返回 true 然后返回 false,对于给定的键,get 方法返回一个值,但接着返回 null,对于以前出现在映射中的键,put 方法返回 null,而 remove 方法返回 false,对于键 set、值 collection 和条目 set 进行的检查,生成的元素数量越来越少。
  • WeakHashMap 中的每个键对象间接地存储为一个弱引用的指示对象。因此,不管是在映射内还是在映射之外,只有在垃圾回收器清除某个键的弱引用之后,该键才会自动移除。
  • 实现注意事项:WeakHashMap 中的值对象由普通的强引用保持。因此应该小心谨慎,确保值对象不会直接或间接地强引用其自身的键,因为这会阻止键的丢弃。注意,值对象可以通过 WeakHashMap 本身间接引用其对应的键;这就是说,某个值对象可能强引用某个其他的键对象,而与该键对象相关联的值对象转而强引用第一个值对象的键。处理此问题的一种方法是,在插入前将值自身包装在 WeakReferences 中,如:m.put(key, new WeakReference(value)),然后,分别用 get 进行解包。
IdentityHashMap(非同步)
  • 此类利用哈希表实现 Map 接口,比较键(和值)时使用引用相等性代替对象相等性。换句话说,在 IdentityHashMap 中,当且仅当(k1==k2)时,才认为两个键 k1 和 k2 相等(在正常 Map 实现(如 HashMap)中,当且仅当满足下列条件时才认为两个键 k1 和 k2 相等:(k1==null ? k2==null : e1.equals(e2)))
  • 此类不是通用 Map 实现!此类实现 Map 接口时,它有意违反 Map 的常规协定,该协定在比较对象时强制使用 equals 方法。此类设计仅用于其中需要引用相等性语义的罕见情况。
  • 此类的典型用法是拓扑保留对象图形转换,如序列化或深层复制。要执行这样的转换,程序必须维护用于跟踪所有已处理对象引用的“节点表”。节点表一定不等于不同对象,即使它们偶然相等也如此。此类的另一种典型用法是维护代理对象。例如,调试设施可能希望为正在调试程序中的每个对象维护代理对象。
  • 此类提供所有的可选映射操作,并且允许 null 值和 null 键。此类对映射的顺序不提供任何保证;特别是不保证顺序随时间的推移保持不变。
PriorityQueue(非同步)
  • PriorityQueue是个基于优先级堆的极大优先级队列(从小到大排列)此队列按照在构造时所指定的顺序对元素排序,既可以根据元素的自然顺序来指定排序也可以根据 Comparator 来指定,这取决于使用哪种构造方法。优先级队列不允许 null 元素
  • 可以在构造时候指定容量和比较器:PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
  • PriorityQueue的内部结构:PriorityQueue中的元素在逻辑上构成了一棵完全二叉树,但是在实际存储时转换为了数组保存在内存中,而我们的PriorityQueue继承了接口Queue,表明这是一个队列,只是它不像普通队列(例如:LinkedList)在遍历输出的时候简单的按顺序从头到尾输出,PriorityQueue总是先输出根节点的值,然后调整树使之继续成为一棵完全二叉树这样每次输出的根节点总是整棵树优先级最高的,要么数值最小要么数值最大。
  • 此实现不是同步的。不是线程安全的。如果多个线程中的任意线程从结构上修改了列表, 则这些线程不应同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。
  • 此实现为排队和出队方法(offer、poll、remove() 和 add)提供 O(log(n)) 时间;为 remove(Object) 和 contains(Object) 方法提供线性时间;为获取方法(peek、element 和 size)提供固定时间。

集合比较

Vector & ArrayList
  • Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
  • 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
Hashtable & HashMap
  1. HashMap 的 key 和 value 都允许为 null,而 Hashtable 和 concurrentHashMap 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。

    • 这样做的原因是:hashtable,concurrenthashmap它们是用于多线程并发的,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,而用于单线程状态的hashmap却可以先get(key)再containKey(key)来确定是否存在(对于hashtable与concurrenthashmap就没办法保证两次操作的原子性,hashmap用于单线程就不需要考虑原子性)。
  2. 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步。使用Collections.synchronizedMap()方法来获取一个线程安全的集合,实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,实际上操作的是传入的HashMap实例,即synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步。

  3. HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1

  4. 两者计算hash的方法不同
    Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模

    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    
  • HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模

    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

迭代器

  • A:迭代器原理
    • 迭代器原理:迭代器是对集合进行遍历,而每一个集合内部的存储结构都是不同的,所以每一个集合存和取都是不一样,那么就需要在每一个类中定义hasNext()和next()方法,这样做是可以的,但是会让整个集合体系过于臃肿,迭代器是将这样的方法向上抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做的好处有二,第一规定了整个集合体系的遍历方式都是hasNext()和next()方法,第二,代码有底层内部实现,使用者不用管怎么实现的,会用即可
  • ListIterator
    • boolean hasNext()是否有下一个
    • boolean hasPrevious()是否有前一个
    • Object next()返回下一个元素
    • Object previous();返回上一个元素
  • Iterator的remove方法从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)。每次调用 next 只能调用一次此方法。如果进行迭代时用调用此方法之外的其他方式修改了该迭代器所指向的 collection,则迭代器的行为是不确定的。如果尚未调用 next 方法,或者在上一次调用 next 方法之后已经调用了 remove 方法。会抛出IllegalStateException异常

List的三个子类的特点

  • ArrayList:底层数据结构是数组,查询快,增删慢。线程不安全,效率高。
  • Vector:底层数据结构是数组,查询快,增删慢。线程安全,效率低。
  • Vector相对ArrayList查询慢(线程安全的),Vector相对LinkedList增删慢(数组结构)
  • LinkedList:底层数据结构是链表,查询慢,增删快。线程不安全,效率高。
  • 使用场景
    • 查询多用ArrayList
    • 增删多用LinkedList
    • 如果都多ArrayList
  • 数组与list之间的转化
    • Arrays工具类的asList()方法的使用:Arrays.asList(T… a) 它的实现是return new ArrayList<>(a); ArrayList为Arrays内部定义的一个类。
      • 该方法对于基本数据类型的数组支持并不好,当数组是基本数据类型时不建议使用,如果传入的是一个基本类型数组,则得到的是这个类型数组的list而不是这个该类型的list集合。
      • 当使用asList()方法时,数组就和列表链接在一起了。当更新其中之一时,另一个将自动获得更新,因为asList获得List实际引用的就是数组
      • asList返回的List是Array中的实现的内部类,而该类并没有定义add和remove方法。
    • Collection中toArray(T[] a)泛型版的集合转数组

泛型类的概述及使用

  • 泛型类
    • 定义格式:public class 类名<泛型类型1,…>
    • 注意事项:泛型类型必须是引用类型
  • 泛型方法
    • 定义格式 :public <泛型类型> 返回类型 方法名(泛型类型 变量名)
  • 泛型接口概述
    • 定义格式 :public interface 接口名<泛型类型>
  • 泛型通配符<?>:任意类型,如果没有明确,那么就是Object以及任意的Java类了
    • ? extends E:向下限定,E及其子类
    • ? super E:向上限定,E及其父类

三种迭代的能否删除

  • 普通for循环,可以删除,但是索引要–
  • 迭代器,可以删除,但是必须使用迭代器自身的remove方法,否则会出现并发修改异常
  • 增强for循环不能删除

源码分析

ArrayList源码解析

定义
  • public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable
  • ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
  • 和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
ArrayList属性
// 保存ArrayList中数据的数组
private transient Object[] elementData;
// ArrayList中实际数据的数量
private int size;
  • ArrayList包含了两个重要的对象:elementData 和 size。
    • elementData 是”Object[]类型的数组”,它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长.
    • size 则是动态数组的实际大小
ArrayList构造函数
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    // 新建一个数组
    this.elementData = new Object[initialCapacity];
}

// ArrayList构造函数。默认容量是10。
public ArrayList() {
    this(10);
}

// 构造一个包含指定元素的list,这些元素的是按照Collection的迭代器返回的顺序排列的
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}
  • 第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。
  • 第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。
  • 第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[]
重要方法
  • add时若指定位置,即将数据插入到指定位置,后面的数据会发生移动。对于没有指定位置的add只需要扩容检查并将数据放入对应数组位置即可。
public void add(int index, E element) {
    // 判断索引是否越界,这里会抛出多么熟悉的异常。。。
    if (index > size || index < 0)
       throw new IndexOutOfBoundsException(
           "Index: "+index+", Size: " +size);

   // 进行扩容检查
   ensureCapacity( size+1);  // Increments modCount  
   // 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置
   System. arraycopy(elementData, index, elementData, index + 1, size - index);
   // 将指定的index位置赋值为element
    elementData[index] = element;
   // list容量+1
    size++;
}
/**
 * 数组容量检查,不够时则进行扩容
 */
public void ensureCapacity( int minCapacity) {
    modCount++;
   // 当前数组的长度
    int oldCapacity = elementData .length;
   // 最小需要的容量大于当前数组的长度则进行扩容
    if (minCapacity > oldCapacity) {
       Object oldData[] = elementData;
      // 新扩容的数组长度为旧容量的1.5倍+1
       int newCapacity = (oldCapacity * 3)/2 + 1;
      // 如果新扩容的数组长度还是比最小需要的容量小,则以最小需要的容量为长度进行扩容
       if (newCapacity < minCapacity)
          newCapacity = minCapacity;
        // minCapacity is usually close to size, so this is a win:
        // 进行数据拷贝,Arrays.copyOf底层实现是System.arrayCopy()
        elementData = Arrays.copyOf( elementData, newCapacity);
   }
}
  • 两个主要方法:一个是在添加时的数组复制一个是数组扩容,这两个操作都是极费效率的,最槽情况下(添加到list第一个位置,删除list最后一个元素或删除list第一个索引位置的元素)时间复杂度可达O(n)。两个方法内部使用的Arrays.copyOf()底层还是调用了System.arrayCopy()方法。只是System.arrayCopy()方法没有返回值,将新数组和旧数组都当做参数传入。
总结:
  • ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
  • 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
  • ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
  • ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
三种遍历方式
  • ArrayList支持3种遍历方式
    • 第一种,通过迭代器遍历。即通过Iterator去遍历。
    • 第二种,随机访问,通过索引值去遍历。由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
    • 第三种,foreach循环遍历。
  • 经过测试发现,遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低!

LinkedList源码解析

定义
  • public class LinkedList
    extends AbstractSequentialList
    implements List, Deque, Cloneable, java.io.Serializable
  • LinkedList 是一个继承于AbstractSequentialList的双向循环链表。它也可以被当作堆栈、队列或双端队列进行操作。
  • LinkedList 实现 List 接口,能对它进行队列操作。
  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  • LinkedList 是非同步的。
属性
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
  • LinkedList中提供了上面两个属性,其中size和ArrayList中一样用来计数,表示list的元素数量,而header则是链表的头结点,Entry则是链表的节点对象。Entry为LinkedList 的内部类,其中定义了当前存储的元素。
private static class Entry<E> {
	E element;  // 当前存储元素
    Entry<E> next;  // 下一个元素节点
    Entry<E> previous;  // 上一个元素节点
    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}
构造函数
/**
* 构造一个空的LinkedList .
*/
public LinkedList() {
	//将header节点的前一节点和后一节点都设置为自身
	header.next = header. previous = header ;
}

/**
* 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列
*/
public LinkedList(Collection<? extends E> c) {
	this();
	addAll(c);
}
  • 空的LinkedList构造方法,它将header节点的前一节点和后一节点都设置为自身,这里便说明LinkedList 是一个双向循环链表,如果只是单存的双向链表而不是循环链表则header节点的前一节点和后一节点均为null。
重要方法
  • LinkedList中header作为双向循环链表的头结点是不保存数据的,也就是说hedaer中的element永远等于null
  • 向链表中增加元素的核心逻辑:
    1. 将元素转换为链表节点
    2. 增加该节点的前后引用(即pre和next分别指向哪一个节点)
    3. 前后节点对该节点的引用(前节点的next指向该节点,后节点的pre指向该节点)。
  • 最重要的entry方法,根据索引位置返回节点,在修改删除等方法中多次被使用到。LinkedList是通过从header开始index计为0,然后一直往下遍历(next),直到到底index位置。为了优化查询效率,LinkedList采用了二分查找(这里说的二分只是简单的一次二分),判断index与size中间位置的距离,采取从header向后还是向前查找。基于双向循环链表实现的LinkedList,通过索引Index的操作时低效的,index所对应的元素越靠近中间所费时间越长。而向链表两端插入和删除元素则是非常高效的(如果不是两端的话,都需要对链表进行遍历查找)。
/**
 * 返回指定索引位置的节点
 */
private Entry<E> entry( int index) {
    // 越界检查
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException( "Index: "+index+
                                            ", Size: "+size );
    // 取出头结点
    Entry<E> e = header;
    // size>>1右移一位代表除以2,这里使用简单的二分方法,判断index与list的中间位置的距离
    if (index < (size >> 1)) {
        // 如果index距离list中间位置较近,则从头部向后遍历(next)
        for (int i = 0; i <= index; i++)
            e = e. next;
    } else {
        // 如果index距离list中间位置较远,则从头部向前遍历(previous)
        for (int i = size; i > index; i--)
            e = e. previous;
    }
    return e;
}
总结:
  • LinkedList 实际上是通过双向链表去实现的。它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  • 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  • LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  • LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  • 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。
  • LinkedList以及ArrayList的迭代效率比较:ArrayList使用最普通的for循环遍历比较快,LinkedList使用foreach循环比较快。ArrayList是实现了RandomAccess接口而LinkedList则没有实现这个接口。LinkedList如果使用普通for循环+list.get(索引)遍历其速度会非常慢, LinkedList的foreach循环编译器默认会使用这个集合的Iterator,速度比for循环快很多。
ArrayList和LinkedList的比较
  1. 顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList
  2. 基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存
  3. 有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:
    • LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址
    • ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址
    • 所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。
  • 从这个分析看出,如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你删除、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。

HashMap源码解析

散列
  • HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
  • 与hashcode有关的定义,我们可以抽出成以下几个关键点:
    • hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的
    • 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
    • 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
    • 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
    • hashCode是用于查找存放元素的桶,而equals是用于比较两个对象的是否相等的。
定义
  • public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
  • HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
  • HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
属性
// 默认初始容量为16,必须为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// Entry数组,长度必须为2的n次幂
transient Entry[] table;
// 已存储元素的数量
transient int size ;
// 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor
int threshold;
// 加载因子
final float loadFactor ;
  • HashMap是通过”拉链法”实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
    • table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的”key-value键值对”都是存储在Entry数组中的。
    • size是HashMap的大小,它是HashMap保存的键值对的数量。
    • threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=”容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
    • loadFactor就是加载因子。
    • modCount是用来实现fail-fast机制的。
  • 可以看出HashMap底层是用Entry数组存储数据,同时定义了初始容量,最大容量,加载因子等参数,至于为什么容量必须是2的幂,加载因子又是什么,下面再说,先来看一下Entry的定义。
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key ; 
        V value;
        Entry<K,V> next; // 指向下一个节点
        final int hash;

        Entry( int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key ;
        }

        public final V getValue() {
            return value ;
        }

        public final V setValue(V newValue) {
           V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key ==null   ? 0 : key.hashCode()) ^
                   ( value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        // 当向HashMap中添加元素的时候调用这个方法,这里没有实现是供子类回调用
        void recordAccess(HashMap<K,V> m) {
        }

        // 当从HashMap中删除元素的时候调动这个方法 ,这里没有实现是供子类回调用
        void recordRemoval(HashMap<K,V> m) {
        }
}
  • Entry是HashMap的内部类,它继承了Map中的Entry接口,它定义了键(key),值(value),和下一个节点的引用(next),以及hash值。很明确的可以看出Entry是什么结构,它是单线链表的一个节点。也就是说HashMap的底层结构是一个数组,而数组的元素是一个单向链表。
  • List中查询时需要遍历所有的数组,为了解决这个问题HashMap采用hash算法将key散列为一个int值,这个int值对应到数组的下标,再做查询操作的时候,拿到key的散列值,根据数组下标就能直接找到存储在数组的元素。但是由于hash可能会出现相同的散列值,为了解决冲突,HashMap采用将相同的散列值存储到一个链表中,也就是说在一个链表中的元素他们的散列值绝对是相同的。找到数组下标取出链表,再遍历链表比直接全部遍历要高效很多。
构造函数
  • HashMap提供了四个构造函数:
    • HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
    • HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
    • HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
    • public HashMap(Map<? extends K, ? extends V> m):包含“子Map”的构造函数
  • 在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
public HashMap( int initialCapacity, float loadFactor) {
    // 初始容量和加载因子合法校验
    if (initialCapacity < 0)
        throw new IllegalArgumentException( "Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException( "Illegal load factor: " + loadFactor);

    // Find a power of 2 >= initialCapacity
    // 确保容量为2的n次幂,是capacity为大于initialCapacity的最小的2的n次幂
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    // 赋值加载因子
    this.loadFactor = loadFactor;
    // 赋值扩容临界值
    threshold = (int)(capacity * loadFactor);
    // 初始化hash表
    table = new Entry[capacity];
    init();
}
  • 上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。
  • initialCapacity:创建 HashMap 时指定的 initialCapacity 并不等于 HashMap 的实际容量,通常来说,HashMap 的实际容量总比 initialCapacity 大一些,除非我们指定的 initialCapacity 参数值恰好是 2 的 n 次方。当然,掌握了 HashMap 容量分配的知识之后,应该在创建 HashMap 时将 initialCapacity 参数值指定为 2 的 n 次方,这样可以减少系统的计算开销。
重要方法
  • get方法:当key为null时会进行特殊处理,在table[0]的链表上查找key为null的元素。get的过程是先计算hash然后通过hash与table.length取模计算index值,然后遍历table[index]上的链表,直到找到key,然后返回
  • 当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
public V put(K key, V value) 
 { 
     // 如果 key 为 null,调用 putForNullKey 方法进行处理
     if (key == null) 
         return putForNullKey(value); 
     // 根据 key 的 keyCode 计算 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; 
         // 找到指定 key 与需要放入的 key 相等(hash 值相同
         // 通过 equals 比较放回 true)
         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; 
 }
  • hash(),这个方法是一个纯粹的数学计算,传入的是对象的hashcode()用于得到hash码,其方法如下:
static int hash(int h) 
{ 
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4); 
}
  • 对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 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 次方.
  • 当 length 总是 2 的倍数时,h & (length-1)将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
  • 根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
  • 上面程序中还调用了 addEntry(hash, key, value, i); 代码,其中 addEntry 是 HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:
void addEntry(int hash, K key, V value, int bucketIndex) 
{ 
    // 获取指定 bucketIndex 索引处的 Entry 
    Entry<K,V> e = table[bucketIndex];      // ①
    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    // 如果 Map 中的 key-value 对的数量超过了极限
    if (size++ >= threshold) 
        // 把 table 对象的长度扩充到 2 倍。
        resize(2 * table.length);      // ②
}
  • 上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
  • resize方法:resize方法在hashmap中并没有公开,这个方法实现了非常重要的hashmap扩容,具体过程为:先创建一个容量为table.length2的新table,修改临界值,然后把table里面元素计算hash值并使用hash与table.length2重新计算index放入到新的table里面。这里需要注意下是用每个元素的hash全部重新计算index,而不是简单的把原table对应index位置元素简单的移动到新table对应位置
void resize( int newCapacity) {
    // 当前数组
    Entry[] oldTable = table;
    // 当前数组容量
    int oldCapacity = oldTable.length ;
    // 如果当前数组已经是默认最大容量MAXIMUM_CAPACITY ,则将临界值改为Integer.MAX_VALUE 返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 使用新的容量创建一个新的链表数组
    Entry[] newTable = new Entry[newCapacity];
    // 将当前数组中的元素都移动到新数组中
    transfer(newTable);
    // 将当前数组指向新创建的数组
    table = newTable;
    // 重新计算临界值
    threshold = (int)(newCapacity * loadFactor);
}

/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable) {
    // 当前数组
    Entry[] src = table;
    // 新数组长度
    int newCapacity = newTable.length ;
    // 遍历当前数组的元素,重新计算每个元素所在数组位置
    for (int j = 0; j < src. length; j++) {
        // 取出数组中的链表第一个节点
        Entry<K,V> e = src[j];
        if (e != null) {
            // 将旧链表位置置空
            src[j] = null;
            // 循环链表,挨个将每个节点插入到新的数组位置中
            do {
                // 取出链表中的当前节点的下一个节点
                Entry<K,V> next = e. next;
                // 重新计算该链表在数组中的索引位置
                int i = indexFor(e. hash, newCapacity);
                // 将下一个节点指向newTable[i]
                e. next = newTable[i];
                // 将当前节点放置在newTable[i]位置
                newTable[i] = e;
                // 下一次循环
                e = next;
            } while (e != null);
        }
    }
}
  • transfer方法中,由于数组的容量已经变大,也就导致hash算法indexFor已经发生变化,原先在一个链表中的元素,在新的hash下可能会产生不同的散列值,so所有元素都要重新计算后安顿一番。注意在do while循环的过程中,每次循环都是将下个节点指向newTable[i] ,是因为如果有相同的散列值i,上个节点已经放置在newTable[i]位置,这里还是下一个节点的next指向上一个节点(不知道这里是否能理解,画个图理解下吧)。
  • Map中的元素越多,hash冲突的几率也就越大,数组长度是固定的,所以导致链表越来越长,那么查询的效率当然也就越低下了。还记不记得同时数组容器的ArrayList怎么做的,扩容!而HashMap的扩容resize,需要将所有的元素重新计算后,一个个重新排列到新的数组中去,这是非常低效的,和ArrayList一样,在可以预知容量大小的情况下,提前预设容量会减少HashMap的扩容,提高性能。
  • 对于加载因子的作用,如果加载因子越大,数组填充的越满,这样可以有效的利用空间,但是有一个弊端就是可能会导致冲突的加大,链表过长,反过来却又会造成内存空间的浪费。所以只能需要在空间和时间中找一个平衡点,那就是设置有效的加载因子。我们知道,很多时候为了提高查询效率的做法都是牺牲空间换取时间,到底该怎么取舍,那就要具体分析了。
  • hash和indexFor:indexFor方法中的h & (length-1)就相当于h%length,用于计算index也就是在table数组中的下标hash方法是对hashcode进行二次散列,以获得更好的散列值。为了更好理解这里我们可以把这两个方法简化为 int index= key.hashCode()/table.length
JDK8中的HashMap
  • JDK8更新了HashMap内部的实现,当复杂的HashCode数量超过一个临界值后,会以红黑树的形式存放对象,从而将整体的时间复杂度缩小至O(1)到O(log(n))的范围内。前提是存入的对象实现了Comparable。
  • 如果key类没有实现Comparable接口,Java会通过调用tieBreakOrder(Object a,Object b)方法来比较键的顺序。tieBreakOrder(Object a,Object b)方法方法会先通过getClass().getName()比较类名大小,再用System.identityHashCode决定顺序。这一过程的成本相当高,采用这种方式时,JDK7和JDK8基本没有性能差距。
  • 当Key类实现了Comparable接口时,JDK8通过红黑树将HashMap最坏情况下的时间复杂度降低为O(log(n))。目前的临界值为,当HashMap的容量大于64,且重复的HashCode数量达到8的时候,将LinkedList转变为红黑树。如果Key没有实现Comparable,反而成本会更高。

LinkedHashMap源码解析

  • 与HashMap的异同:同样是基于散列表实现,区别是,LinkedHashMap内部多了一个双向循环链表的维护,该链表是有序的,可以按元素插入顺序或元素最近访问顺序(LRU)排列,简单地说:LinkedHashMap=散列表+循环双向链表
  • LinkedHashMap中加入了一个head头结点,将所有插入到该LinkedHashMap中的Entry按照插入的先后顺序依次加入到以head为头结点的双向循环链表的尾部。
  • LinkedHashMap由于继承自HashMap,因此它具有HashMap的所有特性,同样允许key和value为null。除了遍历顺序外,其他特性HashMap和LinkedHashMap基本相同。
  • 实际上就是HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,所有put进来的Entry都保存在哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。
LinkedHashMap属性
  • header:双向循环链表的头结点,整个LinkedHashMap中只有一个header,它将哈希表中所有的Entry贯穿起来,header中不保存key-value对,只保存前后节点的引用

  • accessOrder:双向链表中元素排序规则的标志位。accessOrder为false,表示按插入顺序排序。accessOrder为true,表示按访问顺序排序

    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);
        }
    }
    transient LinkedHashMap.Entry<K,V> head;  
    transient LinkedHashMap.Entry<K,V> tail;
    private final boolean accessOrder; 
    
  • LinkedHashMap中的Entry继承于HashMap.Node并且为HashMap的单向链表Node增加了before和after节点。

    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);
            }
        }
    
LinkedHashMap构造方法
//调用HashMap的构造方法来构造底层的数组  
public LinkedHashMap(int initialCapacity, float loadFactor) {  
    super(initialCapacity, loadFactor);  
    accessOrder = false;    //链表中的元素默认按照插入顺序排序  
}  
  
//加载因子取默认的0.75f  
public LinkedHashMap(int initialCapacity) {  
    super(initialCapacity);  
    accessOrder = false;  
}  
  
//加载因子取默认的0.75f,容量取默认的16  
public LinkedHashMap() {  
    super();  
    accessOrder = false;  
}  
  
//含有子Map的构造方法,同样调用HashMap的对应的构造方法  
public LinkedHashMap(Map<? extends K, ? extends V> m) {  
    super(m);  
    accessOrder = false;  
}  
  
//该构造方法可以指定链表中的元素排序的规则  
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {  
    super(initialCapacity, loadFactor);  
    this.accessOrder = accessOrder;  
}  
LinkedHashMap重要方法
  • 注意源码中的accessOrder标志位,当它false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序;当它为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同,覆盖原有的Entry的情况下调用recordAccess方法),该方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用creatEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了),否则,什么也不做。
  • LinkedHashMap并没有覆写HashMap中的put方法,而是覆写了put方法中调用的addEntry方法和recordAccess方法。当要put进来的Entry的key在哈希表中已经在存在时,会调用recordAccess方法,当该key不存在时,则会调用addEntry方法将新的Entry插入到对应槽的单链表的头部。
// 将“key-value”添加到HashMap中      
public V put(K key, V value) {      
    // 若“key为null”,则将该键值对添加到table[0]中。      
    if (key == null)      
        return putForNullKey(value);      
    // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。      
    int hash = hash(key.hashCode());      
    int i = indexFor(hash, table.length);      
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;      
        // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!      
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {      
            V oldValue = e.value;      
            e.value = value;      
            e.recordAccess(this);      
            return oldValue;      
        }      
    }      
  
    // 若“该key”对应的键值对不存在,则将“key-value”添加到table中      
    modCount++;    
    //将key-value添加到table[i]处    
    addEntry(hash, key, value, i);      
    return null;      
}      
  • recordAccess方法:该方法会判断accessOrder是否为true,如果为true,它会将当前访问的Entry(在这里指put进来的Entry)移动到双向循环链表的尾部,从而实现双向链表中的元素按照访问顺序来排序(最近访问的Entry放到链表的最后,这样多次下来,前面就是最近没有被访问的元素,在实现、LRU算法时,当双向链表中的节点数达到最大值时,将前面的元素删去即可,因为前面的元素是最近最少使用的),否则什么也不做。
//覆写HashMap中的recordAccess方法(HashMap中该方法为空),  
//当调用父类的put方法,在发现插入的key已经存在时,会调用该方法,  
//调用LinkedHashmap覆写的get方法时,也会调用到该方法,  
//该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部,  
//accessOrder为true时,get方法会调用recordAccess方法  
//put方法在覆盖key-value对时也会调用recordAccess方法  
//它们导致Entry最近使用,因此将其移到双向链表的末尾  
      void recordAccess(HashMap<K,V> m) {  
          LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    //如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部,  
    //如果是按照插入的先后顺序排序,则不做任何事情。  
          if (lm.accessOrder) {  
              lm.modCount++;  
        //移除当前访问的Entry  
              remove();  
        //将当前访问的Entry插入到链表的尾部  
              addBefore(lm.header);  
          }  
      }  
  • addEntry方法:同样是将新的Entry插入到table中对应槽所对应单链表的头结点中,但可以看出,在createEntry中,同样把新put进来的Entry插入到了双向链表的尾部,从插入顺序的层面来说,新的Entry插入到双向链表的尾部,可以实现按照插入的先后顺序来迭代Entry,而从访问顺序的层面来说,新put进来的Entry又是最近访问的Entry,也应该将其放在双向链表的尾部。
//覆写HashMap中的addEntry方法,LinkedHashmap并没有覆写HashMap中的put方法,  
//而是覆写了put方法所调用的addEntry方法和recordAccess方法,  
//put方法在插入的key已存在的情况下,会调用recordAccess方法,  
//在插入的key不存在的情况下,要调用addEntry插入新的Entry  
   void addEntry(int hash, K key, V value, int bucketIndex) {  
    //创建新的Entry,并插入到LinkedHashMap中  
       createEntry(hash, key, value, bucketIndex);  
  
       //双向链表的第一个有效节点(header后的那个节点)为近期最少使用的节点  
       Entry<K,V> eldest = header.after;  
    //如果有必要,则删除掉该近期最少使用的节点,  
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。  
       if (removeEldestEntry(eldest)) {  
           removeEntryForKey(eldest.key);  
       } else {  
        //扩容到原来的2倍  
           if (size >= threshold)  
               resize(2 * table.length);  
       }  
   }  
  
   void createEntry(int hash, K key, V value, int bucketIndex) {  
    //创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同  
       HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
       table[bucketIndex] = e;  
    //每次插入Entry时,都将其移到双向链表的尾部,  
    //这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素,  
    //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现  
       e.addBefore(header);  
       size++;  
   }  
  • removeEldestEntry方法:该方法默认返回false,我们一般在用LinkedHashMap实现LRU算法时,要覆写该方法,一般的实现是,当设定的内存(这里指节点个数)达到最大值时,返回true,这样put新的Entry(该Entry的key在哈希表中没有已经存在)时,就会调用removeEntryForKey方法,将最近最少使用的节点删除(head后面的那个节点,实际上是最近没有使用)。
//该方法是用来被覆写的,一般如果用LinkedHashmap实现LRU算法,就要覆写该方法,  
//比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中put  
//Entry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。  
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {  
        return false;  
    } 
  • LinkedHashMap覆写了HashMap的get方法:先取得Entry,如果不为null,一样调用recordAccess方法.
//覆写HashMap中的get方法,通过getEntry方法获取Entry对象。  
//注意这里的recordAccess方法,  
//如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做,  
//如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。  
   public V get(Object key) {  
       Entry<K,V> e = (Entry<K,V>)getEntry(key);  
       if (e == null)  
           return null;  
       e.recordAccess(this);  
       return e.value;  
   }  
  • LinkedHashMap是如何实现LRU的。首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。
  • 对LinkedHashMap进行遍历的策略:
    • 按插入顺序遍历:从 header.after 指向的Entry对象开始,然后一直沿着此链表遍历下去,直到某个entry.after == header 为止,完成遍历。在此模式下,如果新插入的<key,value> 对应的key已经存在,对应的Entry在遍历顺序中的位置并不会改变。
    • Get读取顺序:如果LinkedHashMap的这个Get读取遍历顺序开启,那么,当在LinkedHashMap上调用get(key) 方法时,会导致内部 key对应的Entry在双向链表中的位置移动到双向链表的最后。在此模式下,如果新插入的<key,value> 对应的key已经存在,对应的Entry在遍历顺序中的位置会移动到双向链表的最后。

TreeMap源码解析

  • TreeMap是基于红黑树结构实现的一种Map,红黑树通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则:
    • 每个节点都只能是红色或者黑色
    • 根节点是黑色
    • 每个叶节点(NIL节点,空节点)是黑色的。
    • 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
    • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
TreeMap定义
  • public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
  • 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-fast的。
TreeMap属性
// 比较器
private final Comparator<? super K> comparator;
// 红黑树根节点
private transient Entry<K,V> root = null;
// 集合元素数量
private transient int size = 0;
// "fail-fast"集合修改记录
private transient int modCount = 0;
  • TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量: root, size, comparator。
  • root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。
  • 红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。
  • size是红黑数中节点的个数。

HashSet源码解析

定义
  • public class HashSet
    extends AbstractSet
    implements Set, Cloneable, java.io.Serializable
  • HashSet 是一个没有重复元素的集合。
    它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。
HashSet属性
// 底层使用HashMap来保存HashSet的元素
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();
  • hashset中使用一个静态的常量Object类来充当HashMap的value,既然这里map的value是没有意义的,为什么不直接使用null值来充当value的原因是:Java首先将变量PRESENT分配在栈空间,而将new出来的Object分配到堆空间,这里的new Object()是占用堆内存的(一个空的Object对象占用8byte),而null值我们知道,是不会在堆空间分配内存的。为了避免出现异常类java.lang.NullPointerException,hashset采用new Object();作为值。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值