Java 容器

1. 总体框架

总体框架

  • Java集合是java提供的工具包,Java集合工具包位置是java.util.*。
  • Java 集合主要可以划分为 4 个部分:List 列表、Set 集合、Map 映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)
    在这里插入图片描述
    大致说明:

看上面的框架图,先抓住它的主干,即Collection和Map。

  1. Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。
    Collection包含了List和Set两大分支。
    (1)List 是一个有序队列,每一个元素都有它的索引。第一个元素的索引是 0。List的实现类有LinkedList, ArrayList, Vector, Stack。
    (2)Set是一个不允许有重复元素的集合。Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。

  2. Map是一个映射接口,即key-value键值对。Map中的每一个元素包含“一个key”和“key对应的value”。

  3. Iterator,它是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合。我们说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。ListIterator是专门为遍历List而存在的。

  4. Enumeration,它是JDK 1.0引入的抽象类。作用和Iterator一样,也是遍历集合;但是Enumeration的功能要比Iterator少。在上面的框图中,Enumeration只能在Hashtable, Vector, Stack中使用。

  5. Arrays和Collections,它们是操作数组、集合的两个工具类。

2. Collection 架构

Collection架构

List 介绍
public interface List<E> extends Collection<E>{}

List是一个继承于Collection的接口,即List是集合中的一种。List 是有序队列,List 中的每一个元素都有一个索引;第一个元素的索引是 0,往后的元素的索引值依次+1。和Set不同,List中允许有重复的元素。

Set 介绍
public interface Set<E> extends Collection<E> {}

Set是一个继承于Collection的接口,即Set也是集合中的一种。Set是没有重复元素的集合。

Iterator
public interface Iterator<E> {}

Iterator是一个接口,它是集合的迭代器。集合可以通过Iterator去遍历集合中的元素。Iterator提供的API接口,包括:是否存在下一个元素、获取下一个元素、删除当前元素。
注意:Iterator遍历Collection时,是fail-fast机制的。即,当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。关于fail-fast的详细内容,我们会在后面专门进行说明。TODO

abstract boolean hasNext()
abstract E next()
abstract void remove()

3. ArrayList

ArrayList 简介

ArrayList

  1. ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
  2. 和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
  3. ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。
ArrayList 的数据结构

ArrayList包含了两个重要的对象:elementData 和 size。
(01) elementData 是"Object[]类型的数组",它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。
(02) size 则是动态数组的实际大小。

总结

(01) ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
(02) 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
(03) ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
(04) ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

ArrayList 的遍历方式

ArrayList支持3种遍历方式
(01) 第一种,通过迭代器遍历。即通过Iterator去遍历。

Integer value = null;
Iterator iter = list.iterator();
while(iter.hasNext()){
    value = (Integer)iter.next();
}

(02) 第二种,随机访问,通过索引值去遍历。
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。

Integer value = null;
int size = list.size();
for(int i = 0; i < size; i++){
    value = (Integer)list.get(i);
}

(03) 第三种,for循环遍历。如下:(其实就是实现了Iterator)

Integer value = null;
for(Integer integ : list){
    value = integ;
}

4. LinkedList

LinkedList

LinkedList介绍
  1. LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
  2. LinkedList 实现 List 接口,能对它进行队列操作。
  3. LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  4. LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  5. LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  6. LinkedList 是非同步的。
LinkedList的API
boolean       add(E object) 在最后的位置放入元素
boolean       contains(Object object) 是否包含某个元素
E             get(int location) 通过位置获取元素
E             getFirst()
E             getLast()
boolean       offer(E o) 为queue时才使用,将元素插入到queue中
E             peek() 返回列表的头元素
E             poll() 检索并移除此队列的头元素(第一个元素)
E             pop() 表示返回栈顶的元素,同时该元素从栈中删除
void          push(E e) 将元素 e 入栈
Object[]      toArray()
LinkedList数据结构

LinkedList的本质是双向链表。
(01) LinkedList 继承于AbstractSequentialList,并且实现了Dequeue接口。
(02) LinkedList包含两个重要的成员:header 和 size。header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。 size是双向链表中节点的个数。

总结

(01) LinkedList 实际上是通过双向链表去实现的。它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。

LinkedList遍历方式

LinkedList支持多种遍历方式。建议不要采用随机访问的方式去遍历LinkedList,而采用逐个遍历的方式。
(01) 第一种,通过迭代器遍历。即通过Iterator去遍历。

Integer value = null;
Iterator iter = list.iterator();
while(iter.hasNext()){
    value = (Integer)iter.next();
}

6. Vector

Vector 简介
  1. Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
  2. Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。
  3. Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
  4. Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
  5. 和ArrayList不同,Vector中的操作是线程安全的。
Vector 数据结构

Vector的数据结构和ArrayList差不多,它包含了3个成员变量:elementData, elementCount, capacityIncrement。

  1. elementData 是"Object[]类型的数组",它保存了添加到Vector中的元素。elementData是个动态数组,如果初始化Vector时,没指定动态数组的>大小,则使用默认大小10。
  2. elementCount 是动态数组的实际大小。
  3. apacityIncrement 是动态数组的增长系数。如果在创建Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增加时>,增加的大小都是capacityIncrement。
总结
  1. Vector实际上是通过一个数组去保存数据的。当我们构造Vecotr时;若使用默认构造函数,则Vector的默认容量大小是10。
  2. 当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数 >0,则将容量的值增加“容量增加系数”;否则,将容量大小增加一倍。
  3. Vector的克隆函数,即是将全部元素克隆到一个数组中。
Vector 的遍历方式
  1. 第一种,通过迭代器遍历。即通过Iterator去遍历。
Integer value = null;
Iterator iter = vector.iterator();
while(iter.hasNext()){
    value = (Integer)iter.next();
}
  1. 第二种,随机访问,通过索引值去遍历。
Integer value = null;
int size = vec.size();
for(int i = 0; i < size; i++){
    value = (Integer)vec.get(i);
}

8. Stack

Stack介绍

Stack是栈。它的特性是:先进后出(FILO, First In Last Out)。
java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。
当然,我们也可以将LinkedList当作栈来使用!

Stack 的 API
boolean                     empty()
synchronized  E             peek()
synchronized  E             pop()
E                           push(E object)
总结

(01) Stack实际上也是通过数组去实现的。
执行push时(即,将元素推入栈中),是通过将元素追加的数组的末尾中。
执行peek时(即,取出栈顶元素,不执行删除),是返回数组末尾的元素。
执行pop时(即,取出栈顶元素,并将该元素从栈中删除),是取出数组末尾的元素,然后将该元素从数组中删除。
(02) Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有。

8. List 总结

List 使用场景

如果涉及到“栈”、“队列”、“链表”等操作,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。

  1. 对于需要快速插入,删除元素,应该使用LinkedList。
  2. 对于需要快速随机访问元素,应该使用ArrayList。
  3. 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。 对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。
LinkedList 和 ArrayList 性能差异分析

为什么向 LinkedList 中插入元素很快,而 ArrayList 中插入元素很慢?

  • LinkedList 插入结点
// 在index前添加节点,且节点的值为element
public void add(int index, E element) {
    addBefore(element, (index==size ? header : entry(index)));
}

// 获取双向链表中指定位置的节点
private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                            ", Size: "+size);
    Entry<E> e = header;
    // 获取index处的节点。
    // 若index < 双向链表长度的1/2,则从前向后查找;
    // 否则,从后向前查找。
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

// 将节点(节点数据是e)添加到entry节点之前。
private Entry<E> addBefore(E e, Entry<E> entry) {
    // 新建节点newEntry,将newEntry插入到节点e之前;并且设置newEntry的数据是e
    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
    // 插入newEntry到链表中
    newEntry.previous.next = newEntry;
    newEntry.next.previous = newEntry;
    size++;
    modCount++;
    return newEntry;
}

从代码中我们可以看出,通过add(int index, E element)向LinkedList插入元素时,先是在双向链表中找到要插入节点的位置index;找到之后,再插入一个新节点。

双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

  • ArrayList 中插入新结点
/ 将e添加到ArrayList的指定位置
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(
        "Index: "+index+", Size: "+size);
    
    //ensureCapacity(size+1) 的作用是“确认ArrayList的容量,若容量不够,则增加容量。”
    ensureCapacity(size+1);  
    /*
    真正耗时的操作是 System.arraycopy(elementData, index, elementData, index + 1, size - index);
    System.arraycopy(elementData, index, elementData, index + 1, size - index); 会移动index之后所有元素即可。
    因为要在index的位置插入元素,而这个函数会移动index之后的所有元素,所以ArrayList插入很慢
    */
    System.arraycopy(elementData, index, elementData, index + 1,
         size - index);
    elementData[index] = element;
    size++;
}

为什么LinkedList中随机访问很慢,而ArrayList中随机访问很快?

  • LinkedList 随机访问代码
// 返回LinkedList指定位置的元素
public E get(int index) {
    return entry(index).element;
}

// 获取双向链表中指定位置的节点
private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                            ", Size: "+size);
    Entry<E> e = header;
    // 获取index处的节点。
    // 若index < 双向链表长度的1/2,则从前先后查找;
    // 否则,从后向前查找。
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

从中,我们可以看出:通过get(int index)获取LinkedList第index个元素时,先是在双向链表中找到要index位置的元素;找到之后再返回。

双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

  • ArrayList 随机访问代码
// 获取index位置的元素值
public E get(int index) {
    RangeCheck(index);

    return (E) elementData[index];
}

private void RangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(
        "Index: "+index+", Size: "+size);
}

从中,我们可以看出:通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像LinkedList一样进行查找。

Vecotr 和 ArrayList 比较
相同之处
// ArrayList的定义
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

// Vector的定义
public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
  1. 它们都是 List,它们都继承于 AbstractList,并且实现了 Liet 接口。

  2. 它们都实现了 RandomAccess 和 Cloneable 接口
    实现RandomAccess接口,意味着它们都支持快速随机访问;
    实现Cloneable接口,意味着它们能克隆自己。

  3. 它们都是通过数组实现的,本质上都是动态数组
    ArrayList.java中定义数组elementData用于保存元素

// 保存ArrayList中数据的数组
private transient Object[] elementData;

Vector.java中也定义了数组elementData用于保存元素

// 保存Vector中数据的数组
protected Object[] elementData;
  1. 它们的默认数组容量是10
    若创建ArrayList或Vector时,没指定容量大小;则使用默认容量大小10。
    ArrayList的默认构造函数如下:
// ArrayList构造函数。默认容量是10。
public ArrayList() {
    this(10);
}

Vector的默认构造函数如下:

// Vector构造函数。默认容量是10。
public Vector() {
    this(10);
} 
  1. 它们都支持 Iterator 和 listIterator 遍历
不同之处
  1. 线程安全性不一样
    ArrayList是非线程安全;
    而Vector是线程安全的,它的函数都是synchronized的,即都是支持同步的。
    ArrayList适用于单线程,Vector适用于多线程。

  2. 对序列化支持不同
    ArrayList支持序列化,而Vector不支持;即ArrayList有实现java.io.Serializable接口,而Vector没有实现该接口。

  3. 构造函数个数不同
    ArrayList有3个构造函数,而Vector有4个构造函数。Vector除了包括和ArrayList类似的3个构造函数之外,另外的一个构造函数可以指定容量增加系数。

  4. 容量增加方式不同
    逐个添加元素时,若ArrayList容量不足时,“新的容量”=“(原始容量x3)/2 + 1”。

    而Vector的容量增长与“增长系数有关”,若指定了“增长系数”,且“增长系数有效(即,大于0)”;那么,每次容量不足时,“新的容量”=“原始容量+增长系数”。若增长系数无效(即,小于/等于0),则“新的容量”=“原始容量 x 2”。

9. Map 架构

在这里插入图片描述

  1. Map 是映射接口,Map中存储的内容是键值对(key-value)。
  2. AbstractMap 是继承于Map的抽象类,它实现了Map中的大部分API。其它Map的实现类可以通过继承AbstractMap来减少重复编码。
  3. SortedMap 是继承于Map的接口。SortedMap中的内容是排序的键值对,排序的方法是通过比较器(Comparator)。
  4. NavigableMap 是继承于SortedMap的接口。相比于SortedMap,NavigableMap有一系列的导航方法;如"获取大于/等于某对象的键值对"、“获取小于/等于某对象的键值对”等等。
  5. TreeMap 继承于AbstractMap,且实现了NavigableMap接口;因此,TreeMap中的内容是“有序的键值对”!
  6. HashMap 继承于AbstractMap,但没实现NavigableMap接口;因此,HashMap的内容是“键值对,但不保证次序”!
  7. Hashtable 虽然不是继承于AbstractMap,但它继承于Dictionary(Dictionary也是键值对的接口),而且也实现Map接口;因此,Hashtable的内容也是“键值对,也不保证次序”。但和HashMap相比,Hashtable是线程安全的,而且它支持通过Enumeration去遍历。
  8. WeakHashMap 继承于AbstractMap。它和HashMap的键类型不同,WeakHashMap的键是“弱键”。
Map
  1. Map 是一个键值对(key-value)映射接口。Map映射中不能包含重复的键;每个键最多只能映射到一个值。
  2. Map 接口提供三种collection 视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。
  3. Map 映射顺序。有些实现类,可以明确保证其顺序,如 TreeMap;另一些映射实现则不保证顺序,如 HashMap 类。
Map 的 API
abstract boolean              containsKey(Object key)
abstract boolean              containsValue(Object value)
abstract Set<Entry<K, V>>     entrySet()
abstract boolean              isEmpty()
abstract Set<K>               keySet()
abstract V                    put(K key, V value)
abstract int                  size()
abstract Collection<V>        values()
abstract V                    remove(Object key)

说明:

  1. Map提供接口分别用于返回 键集、值集或键-值映射关系集。
    entrySet()用于返回键-值集的Set集合
    keySet()用于返回键集的Set集合
    values()用户返回值集的Collection集合
    因为Map中不能包含重复的键;每个键最多只能映射到一个值。所以,键-值集、键集都是Set,值集时Collection。
  2. Map提供了“键-值对”、“根据键获取值”、“删除键”、“获取容量大小”等方法。

10. HashMap

HashMap扩容

HashMap 简介

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。
HashMap 初始容量为 16。

HashMap 数据结构

HashMap的继承关系

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

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { }
  1. HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
  2. HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
    (1)table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
    (2)size是HashMap的大小,它是HashMap保存的键值对的数量。
    (3)threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
    (4)loadFactor就是加载因子。
    (5)modCount是用来实现fail-fast机制的。

在这里插入图片描述
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

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 扩容机制
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {//当size大于等于某一个阈值thresholdde时候且该桶并不是一个桶;
          /*这个这样说明比较好理解:因为size 已经大于等于阈值了,说明Entry数量较多,哈希冲突严重,那么若该Entry对应的桶不是一个空桶,这个Entry的加入必然会把原来的链表拉得更长,因此需要扩容;若对应的桶是一个空桶,那么此时没有必要扩容。*/
            resize(2 * table.length);//将容量扩容为原来的2倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//扩容后的,该hash值对应的新的桶位置
        }

        createEntry(hash, key, value, bucketIndex);//在指定的桶位置上,创建一个新的Entry
    }

    /**
     * Like addEntry except that this version is used when creating entries
     * as part of Map construction or "pseudo-construction" (cloning,
     * deserialization).  This version needn't worry about resizing the table.
     *
     * Subclass overrides this to alter the behavior of HashMap(Map),
     * clone, and readObject.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);//链表的头插法插入新建的Entry
        size++;//更新size
    }

上面几个重要的成员:

/*
size记录的是map中包含的Entry的数量
*/
transient int size;

/*
而threshold记录的是需要resize的阈值 且 threshold = loadFactor * capacity
capacity 其实就是桶的长度
*/
int threshold;

final float loadFactor;
  • 扩容的时机
    当 map 中包含的 Entry 的数量大于等于 threshold = loadFactor * capacity 的时候,且新建的 Entry 刚好落在非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。

当size大于等于threshold的时候,并不一定会触发扩容机制,但是会很可能就触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize。

HashMap 扩容过程

上面有一个很重要的方法,包含了几乎属于的扩容过程,这就是resize()。

/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历同桶数组中的每一个桶
            while(null != e) {//顺序遍历某个桶的外挂链表
                Entry<K,V> next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                e.next = newTable[i];//头插法插入新表中
                newTable[i] = e;
                e = next;
            }
        }
    }
为什么容量必须是 2 的幂
 while (capacity < initialCapacity)
     capacity <<= 1;

通过以上我们知道HashMap的容量必须是2的幂,那么为什么要这么设计呢?答案当然是为了性能。在HashMap通过键的哈希值进行定位桶位置的时候,调用了一个indexFor(hash, table.length);方法。

  /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

可以看到这里是将哈希值h与桶数组的length-1(实际上也是map的容量-1)进行了一个与操作得出了对应的桶的位置,h & (length-1)。

Java的%、/操作比&慢10倍左右,因此采用&运算会提高性能。

通过限制length是一个2的幂数,h & (length-1)和h % length结果是一致的。这就是为什么要限制容量必须是一个2的幂的原因。

get()

get() 的作用是获取key对应的value,它的实现代码如下:

public V get(Object key) {
     if (key == null)
          return getForNullKey();
     // 获取key的hash值
     int hash = hash(key.hashCode());
     // 在“该hash值对应的链表”上查找“键值等于key”的元素
     for (Entry<K,V> e = table[indexFor(hash, table.length)];
          e != null;
          e = e.next) {
                   Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
             return e.value;
     }
     return null;
put

put() 的作用是对外提供接口,让HashMap对象可以通过put()将“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++;
    addEntry(hash, key, value, i);
    return null;
}

若要添加到HashMap中的键值对对应的key已经存在HashMap中,则找到该键值对;然后新的value取代旧的value,并退出!
若要添加到HashMap中的键值对对应的key不在HashMap中,则将其添加到该哈希值对应的链表中,并调用addEntry()。

HashMap 遍历

4.1 遍历HashMap的键值对
第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

// 假设map是HashMap对象
// map中的key是String类型,value是Integer类型
Integer integ = null;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()){
    Map.Entry entry = (Map.Entry)iter.next();
    // 获取key
    key = (String)entry.getKey();
        // 获取value
    integ = (Integer)entry.getValue();
}

4.2 遍历HashMap的键

第一步:根据keySet()获取HashMap的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

// 假设map是HashMap对象
// map中的key是String类型,value是Integer类型
String key = null;
Integer integ = null;
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
        // 获取key
    key = (String)iter.next();
        // 根据key,获取value
    integ = (Integer)map.get(key);
}

4.3 遍历HashMap的值

第一步:根据value()获取HashMap的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

// 假设map是HashMap对象
// map中的key是String类型,value是Integer类型
Integer value = null;
Collection c = map.values();
Iterator iter= c.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}
为什么 Hashmap 是线程不安全的

为什么 Hashmap 是线程不安全的

  1. resize 机制
    HashMap的扩容机制就是重新申请一个容量是当前的2倍的桶数组,然后将原先的记录逐个重新映射到新的桶里面,然后将原先的桶逐个置为null使得引用失效。后面会讲到,HashMap之所以线程不安全,就是resize这里出的问题。
  2. 可能导致 HashMap 线程不安全的两个地方
    (1)put的时候导致的多线程数据不一致
    这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

(2)另外一个比较明显的线程不安全的问题是 HashMap 的 get 操作可能因为 resize 而引起死循环
下面的代码是resize的核心内容:

void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;  
        for (Entry<K,V> e : table) {  
  
            while(null != e) {  
                Entry<K,V> next = e.next;           
                if (rehash) {  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);   
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            } 
        }  
    }  

这个方法的功能是将原来的记录重新计算在新桶的位置,然后迁移过去。

在这里插入图片描述
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。

Hashmap1.7从链表头插入数据,1.8从链表尾部插入数据,有什么区别为什么

HashMap 在 jdk1.7 中采用头插法,在扩容的时候改变链表中元素的原本顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

HashMap在1.7和1.8之间的变化
  1. 1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
  2. 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置
  3. 1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法
HashMap 中红黑树为什么能提高查询性能,为什么在 HashMap 中使用红黑树

虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把时间控制在O(logn),不过却不是最佳的。因为平衡树要求每个结点的左子树和右子树的高度差至多于等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。

显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
(1)具有二叉查找树的特点
(2)根节点是黑色的
(3)每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据
(4)任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的
(5)每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点
在这里插入图片描述
正是由于红黑树的这种特点,使得它能够在最坏情况下,也能在 O(logn) 的时间复杂度查找到某个节点。

不过,与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。

不过,如果你要说,单单在查找方面的效率的话,平衡树比红黑树快。

所以,我们也可以说,红黑树是一种不大严格的平衡树。也可以说是一个折中发方案。

平衡树是为了解决二叉查找树退化为链表的情况,而红黑树是为了解决平衡树在插入、删除等操作需要频繁调整的情况。

HashMap 线程安全怎么实现
  1. 使用 Hashtable 类
    Hashtable 通过将几个主要操作 get()、 put()、 remove()、 putAll() 和 clear() 使用 synchronized 修饰,所以不会出现两个线程同时对数据进行操作的情况,因此保证了线程的安全性,但是也大大降低了执行效率。因此是不推荐的。

  2. 通过 Collections.synchronizedMap(Map<K,V>) 返回一个新的 Map,这个新的 Map 是线程安全的。
    从实现源代码可以发现,其封装的本质和 Hashtable 的实现是完全一致的,即对原Map本身的方法进行加锁,加锁的对象或者为外部指定共享对象 mutex,或者为包装后的线程安全的Map本身。

  3. 使用 ConcurrentHashMap
    这是 HashMap 的线程安全版,同 Hashtable 相比,ConcurrentHashMap 不仅保证了访问的线程安全性,而且在效率上有较大的提高。

11. ConcurrentHashMap

  1. ConcurrentHashMap的锁分段技术
    ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
    在这里插入图片描述

  2. ConcurrentHashMap的 get 操作
    get 操作的高效之处在于整个 get 过程不需要加锁,除非读到的值是空的才会加锁重读,我们知道HashTable容器的 get 方法是需要加锁的,那么ConcurrentHashMap 的 get 操作是如何做到不加锁的呢?
    原因是它的get方法里将要使用的共享变量都定义成 volatile,如用于统计当前 Segment 大小的 count 字段和用于存储值的 HashEntryvalue。定义成 volatile 的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在 get 操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改或获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

  3. ConcurrentHashMap的 put 操作
    由于put方法需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须得加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。
    是否需要扩容。在插入元素前会先判断Segment里的HashEntry数组是否超过容量的阈值(threshold),如果超过阈值,数组进行扩容。值的一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经达到容量的阈值,如果到达了就进行扩容,但是有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
    如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

  4. ConcurrentHashMap的 size 操作
    如果我们需要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。
    因为在累加 count 操作过程中,之前累加过的 count 发生变化的几率非常小,所以ConcurrentHashMap 的做法是先尝试2次通过不锁住 Segment 的方式来统计各个Segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有 Segment 的大小。
    那么 ConcurrentHashMap 是如何判断在统计的时候容器是否发生了变化呢?使用modCount 变量,在 put,remove 和 clean 方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

12. Hashtable

Hashtable 介绍

和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量是哈希表中桶 的数量,初始容量就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。

Hashtable 数据结构

继承关系

java.lang.Object
   ↳     java.util.Dictionary<K, V>
         ↳     java.util.Hashtable<K, V>

public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable { }
  1. Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
  2. Hashtable是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
    table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
    count是Hashtable的大小,它是Hashtable保存的键值对的数量。
    threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值=“容量*加载因子”。
    loadFactor就是加载因子。
    modCount是用来实现fail-fast机制的

13. TreeMap

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。

TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

TreeMap数据结构
java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.TreeMap<K, V>

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

(01) TreeMap实现继承于AbstractMap,并且实现了NavigableMap接口。
(02) 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是红黑数中节点的个数。

红黑树(一) 原理和算法详细介绍

14. Map 总结

在这里插入图片描述

Map 概况
  1. HashMap 是基于“拉链法”实现的散列表。一般用于单线程程序中。
  2. Hashtable 也是基于“拉链法”实现的散列表。它一般用于多线程程序中。
  3. WeakHashMap 也是基于“拉链法”实现的散列表,它一般也用于单线程程序中。相比HashMap,WeakHashMap中的键是“弱键”,当“弱键”被GC回收时,它对应的键值对也会被从WeakHashMap中删除;而HashMap中的键是强键。
  4. TreeMap 是有序的散列表,它是通过红黑树实现的。它一般用于单线程中存储有序的映射。
HashMap 和 Hashtable 的异同
  • HashMap和Hashtable的相同点

    HashMap和Hashtable都是存储“键值对(key-value)”的散列表,而且都是采用拉链法实现的。
    存储的思想都是:通过table数组存储,数组的每一个元素都是一个Entry;而一个Entry就是一个单向链表,Entry链表中的每一个节点就保存了key-value键值对数据。

    添加key-value键值对:首先,根据key值计算出哈希值,再计算出数组索引(即,该key-value在table中的索引)。然后,根据数组索引找到Entry(即,单向链表),再遍历单向链表,将key和链表中的每一个节点的key进行对比。若key已经存在Entry链表中,则用该value值取代旧的value值;若key不存在Entry链表中,则新建一个key-value节点,并将该节点插入Entry链表的表头位置。
    删除key-value键值对:删除键值对,相比于“添加键值对”来说,简单很多。首先,还是根据key计算出哈希值,再计算出数组索引(即,该key-value在table中的索引)。然后,根据索引找出Entry(即,单向链表)。若节点key-value存在与链表Entry中,则删除链表中的节点即可。

  • HashMap和Hashtable的不同点

  1. 继承和实现方式不同
    HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
    Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。

  2. 线程安全不同
    Hashtable的几乎所有函数都是同步的,即它是线程安全的,支持多线程。
    而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,需要我们额外的进行同步处理。 对HashMap的同步处理可以使用Collections类提供的synchronizedMap静态方法,或者直接使用JDK 5.0之后提供的java.util.concurrent包里的ConcurrentHashMap类。

  3. 对null值的处理不同
    HashMap的key、value都可以为null。
    Hashtable的key、value都不可以为null。

    Hashtable的key或value,都不能为null!否则,会抛出异常NullPointerException。
    HashMap的key、value都可以为null。 当HashMap的key为null时,HashMap会将其固定的插入table[0]位置(即HashMap散列表的第一个位置);而且table[0]处只会容纳一个key为null的值,当有多个key为null的值插入的时候,table[0]会保留最后插入的value。

  4. 容量的初始值 和 增加方式都不一样
    HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”。
    Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。

15. Set

在这里插入图片描述

  1. Set 是继承于Collection的接口。它是一个不允许有重复元素的集合。
  2. AbstractSet 是一个抽象类,它继承于AbstractCollection,AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利。
  3. HastSet 和 TreeSet 是Set的两个实现类。
    HashSet依赖于HashMap,它实际上是通过HashMap实现的。HashSet中的元素是无序的。
    TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。TreeSet中的元素是有序的。

16. HashSet

HashSet 是一个没有重复元素的集合。
它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。
HashSet是非同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

Set s = Collections.synchronizedSet(new HashSet(...));
HashSet的主要API
boolean         add(E object)
void               clear()
Object           clone()
boolean         contains(Object object)
boolean         isEmpty()
Iterator<E>     iterator()
boolean         remove(Object object)
int                size()
HashSet 和 HashMap 的区别
  1. HashSet 实现了 Set 接口,它不允许集合中出现重复元素。当我们提到 HashSet 时,第一件事就是在将对象存储在 HashSet 之前,要确保重写 hashCode() 方法和 equals() 方法,这样才能比较对象的值是否相等,确保集合中没有储存相同的对象。

  2. HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))
    HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。
    public Object put(Object Key,Object value)方法用来将元素添加到map中。

17. TreeSet

TreeSet 介绍

TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet, Cloneable, java.io.Serializable接口。
TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
TreeSet 实现了Cloneable接口,意味着它能被克隆。
TreeSet 实现了java.io.Serializable接口,意味着它支持序列化

TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值