Java核心技术之- 集合

目录

1.Collection和Map的主要API

2.Collection接口下的集合

 2.1 List:有序集合,可以重复

2.1.1 ArrayList

2.1.2 LinkedList

2.1.3 Vector

2.2 Set:无序集合,不能重复

2.2.1 HashSet

2.2.2 LinkedHashSet

2.2.3 TreeSet

2.2.4 红黑树

2.3 Queue:队列,先入先出

2.3.1 ArrayBlockingQueue

2.3.2 LinkedBlockingQueue

2.3.3 PriorityQueue

2.3.4 LinkedList

2.3.5 ArrayDeque

3.Map接口下的集合

3.1 HashMap

3.2 Hashtable

3.3 TreeMap

3.4 LinkedHashMap

4.Iterator

5.集合间的转换

5.1 List <-> Set

5.2 List <-> Queue

5.3 Set <-> SortedSet

5.4 List <-> Map


Java集合分为Collection和Map两大类。

1.Collection和Map的主要API

- add(E):添加元素
- remove(E):删除元素 
- contains(E):判断是否包含某元素
- size():返回元素个数
- isEmpty():判断是否为空
- clear():清空集合
- iterator():获得迭代器
- keySet()/values()/entrySet():获得键/值/键值对集

2.Collection接口下的集合

Java 集合框架提供了一套性能优良、线程安全的集合类

 2.1 List:有序集合,可以重复

List 是 Java 中最基本的集合之一。它是一个有序的集合,可以包含重复元素。
List 的主要特征有:

1. 有序:List 中的元素有明确的顺序。
2. 允许重复:List 可以包含重复的元素。
3. 索引访问:我们可以通过索引访问 List 中的元素。

List 接口定义了一些常用的方法:

- add(element):添加元素
- add(index, element):在指定索引添加元素
- remove(index):移除指定索引的元素
- remove(object):移除首个匹配的元素 
- get(index):获取指定索引的元素
- set(index, element):设置指定索引的元素
- size():返回集合中的元素个数
- isEmpty():判断是否为空
- contains(object):判断是否包含某个元素
- iterator():返回一个迭代器

主要实现有ArrayList、LinkedList和Vector。

示例代码: 
 

 List<Integer> list = new ArrayList<>();
 
 // 添加元素
 list.add(1); 
 list.add(2);
 list.add(3);
 
 // 在指定索引添加元素
 list.add(1, 10);
 
 // 移除元素
 list.remove(0);
 list.remove(Integer.valueOf(2));
 
 // 获取元素
 int first = list.get(0); 
 
 // 设置元素
 list.set(1, 20); 
 
 // 大小和判断是否为空
 int size = list.size();
 boolean isEmpty = list.isEmpty();
 
 // 迭代器遍历
 Iterator<Integer> iterator = list.iterator();
 while (iterator.hasNext()) {
     System.out.println(iterator.next()); 
 }

2.1.1 ArrayList

Java 中最常用的动态数组,基于数组实现,查询快,增删慢。

ArrayList 的实现细节:

1. 存储元素的数组。ArrayList 中维护了一个 Object[] elementData 数组,该数组存储 ArrayList 中的所有元素。

2. 数组容量。ArrayList 还维护了数组的容量(capacity),该容量表示 elementData 数组中最大可以存储的元素个数。

3. 数组实际大小。ArrayList 还维护了数组实际存储的元素个数 size。

4. 扩容机制。当向 ArrayList 添加元素时,如果 size 达到 capacity,ArrayList 会自动扩容,新容量的计算规则为:capacity * 1.5。扩容后的数组拷贝原数组中的所有元素到新数组。

5. 缩容机制。如果 ArrayList 中删除了大量元素,导致 size 远小于 capacity,ArrayList 也会自动缩容,新容量的计算规则为:capacity / 1.5。缩容后的数组同样需要拷贝原数组中的所有元素到新数组。

6. 默认初始容量。ArrayList 的默认初始容量为 10。

7. 空数组。ArrayList 构造方法允许传入初始容量,如果传入 0,那么 ArrayList 中的 elementData 数组初始化为空数组。而不是 null。这是因为要支持 get(index) 方法。

ArrayList 利用动态扩容和缩容机制,保证 elementData 数组有足够的空间存储新增元素,同时尽量不浪费过多空间。这使得 ArrayList 既具有数组的高效访问特性,也具有链表的动态增长特性。

2.1.2 LinkedList


LinkedList 由内部的 Node 结点类构成。它有 first 和 last 指针分别指向链表的头结点和尾结点。通过这两个指针可以在链表的头部和尾部快速添加元素。获取指定位置的元素时,会从头结点或尾结点开始向中间遍历,这样可以提高效率。

LinkedList 通过双向链表实现,支持在链表两端快速添加和删除元素,是一种高效的列表实现。缺点是查询慢。

LinkedList 的实现细节:

1. 内部类 Node: LinkedList 的基本组成单元,包含 prev、next 指针和元素本身。

2. 实例变量:

- first: 指向链表头结点
- last: 指向链表尾结点 
- size: 链表元素个数

3. addFirst(E e) 方法:在链表头部添加元素

- 创建新结点 newNode,prev 指向 null,next 指向原 first 结点
- 若原 first 结点为空,则 last 也指向 newNode
- 否则,原 first 结点的 prev 指向 newNode
- first 指向 newNode,size++

4. addLast(E e) 方法:在链表尾部添加元素

- 创建新结点 newNode,prev 指向原 last 结点,next 指向 null
- 若 orig last 结点为空,则 first 也指向 newNode  
- 否则,原 last 结点的 next 指向 newNode
- last 指向 newNode,size++

5. get(int index) 方法:获取指定位置元素

- 检查索引合法性
- 如果 index 小于 size 的一半,从 first 结点开始遍历
- 否则,从 last 结点开始反向遍历  
- 找到对应结点,返回其元素

6. 其他方法:

- removeFirst() / removeLast(): 删除头结点/尾结点    
- add(E e):将元素添加至尾部  
- remove(int index) :删除指定位置元素
- 等等

2.1.3 Vector

和ArrayList类似,线程安全,效率低。

Vector 的实现细节

1. 默认容量和扩容因子:

- DEFAULT_CAPACITY = 10:默认初始容量  
- CAPACITY_INCREMENT = 10:默认扩容因子,扩容量是原来的1倍或2倍

2. 实例变量:

- elementData:对象数组,用于存储元素
- elementCount:元素个数 
- capacityIncrement:扩容因子

3. 构造方法:

- Vector():默认构造方法,初始容量10,扩容因子10
- Vector(int initialCapacity):指定初始容量,扩容因子10
- Vector(int initialCapacity, int capacityIncrement):指定初始容量和扩容因子

4. addElement(E obj) 方法:向尾部添加元素

- 调用 ensureCapacityHelper(elementCount + 1) 确保至少能容纳 elementCount + 1 个元素,否则扩容
- 将元素 obj 添加至 elementData[elementCount]
- elementCount++

5. ensureCapacityHelper(int minCapacity) 方法:确保至少能容纳 minCapacity 个元素,否则扩容

- 如果需要的 minCapacity 大于当前 elementData.length,则调用 grow(minCapacity) 进行扩容

6. grow(int minCapacity) 方法:进行扩容

- 新容量 newCapacity 为原容量的1倍或2倍,但至少要大于 minCapacity
- 新建一个大小为 newCapacity 的数组,并将原数组内容拷贝过来
- elementData 指向新数组,完成扩容

7. elementAt(int index) 方法:获取指定位置元素

- 检查索引合法性,然后直接从 elementData 获取元素

Vector 是通过数组实现的动态数组,它通过默认容量、扩容因子和扩容机制支持动态增长,能够存放任意类型的元素,同时还保证线程安全,适用于多线程环境下的场景。

2.2 Set:无序集合,不能重复

Set 接口的实现类通过 hash 算法、树结构和链表结构实现元素的唯一性和不同的操作复杂度。

主要实现有HashSet、TreeSet和LinkedHashSet。

2.2.1 HashSet

基于哈希表实现,支持快速查找,添加,删除操作。

 public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
     // 底层采用 HashMap 存储元素
     private transient HashMap<E,Object> map;
 
     // 默认构造函数,实例化 HashMap
     public HashSet() {
         map = new HashMap<>();
     }
 
     // 添加元素
     public boolean add(E e) {
         return map.put(e, PRESENT)==null;
     }
 
     // 删除元素
     public boolean remove(Object o) {
         return map.remove(o)==PRESENT;
     }
 
     // 获取元素个数
     public int size() {
         return map.size();
     }
 
     // 判断是否包含某元素
     public boolean contains(Object o) {
         return map.containsKey(o);
     }
 
     // 遍历元素
     public Iterator<E> iterator() {
         return map.keySet().iterator();
     }
 
     // 清空集合
     public void clear() {
         map.clear();
     }
 }


HashSet 的实现原理:

1. 底层采用 HashMap 存储元素,元素作为键,PRESENT 作为值。

2. 添加元素通过 HashMap 的 put 方法添加键值对实现。

3. 删除元素通过 HashMap 的 remove 方法删除键值对实现。

4.  size()、contains()、clear() 方法直接调用 HashMap 中对应的方法。

5. 迭代器通过 HashMap 的 keySet() 获取 map 的键集合,然后遍历。

6.  HashSet 本身并不存储元素,它通过委托 HashMap 来实现 Set 接口,这是一种典型的装饰器模式。

7. HashSet 的时间空间复杂度与 HashMap 相同,都是 O(1),采用拉链法解决 Hash 碰撞,有一定的存储开销。

HashSet 通过委托 HashMap 实现了 Set 接口,具有和 HashMap 相同的时间空间复杂度,是一种高性能的 Set 实现类。

2.2.2 LinkedHashSet

LinkedHashSet 是一个有序的 Set 集合,它是基于 HashSet 和 LinkedHashMap实现的,迭代顺序为元素的插入顺序。 

LinkedHashSet的实现原理:

1. LinkedHashSet 内部维护一个 LinkedHashMap,该 Map 的 key 是添加到 Set 中的元素,value 是默认值(可以是任意值)。

2. 当我们向 LinkedHashSet 中添加元素时,实际上是把元素添加到内部的 LinkedHashMap 中。

3. LinkedHashMap 是一个有序的 Map,它的顺序是根据元素的添加顺序来维护的。所以,通过内部的 LinkedHashMap,LinkedHashSet 也就保证了元素的添加顺序。

4. LinkedHashSet 重写了 HashSet 的 hashCode、equals 方法,并委托给内部的 LinkedHashMap 来实现。

5. LinkedHashSet 重写了 HashSet 的 iterator 方法,返回一个 LinkedHashMap 的 EntrySet 遍历器,这样我们在遍历 LinkedHashSet 时,就是按照 LinkedHashMap 的顺序进行遍历。

LinkedHashSet 之所以有序,是因为它内部维护了一个 LinkedHashMap,并委托给该 Map 来实现 hashCode、equals、iterator 等方法,从而达到有序的效果。和 HashSet 一样,LinkedHashSet 也不允许集合中出现重复元素。

LinkedHashSet 源码解读:

我们来详细解读一下 LinkedHashSet 的源码。

1. 首先看构造方法,LinkedHashSet 委托给内部的 LinkedHashMap 来初始化

 public LinkedHashSet() {
     map = new LinkedHashMap<>();
 }
 
 public LinkedHashSet(int initialCapacity) {
     map = new LinkedHashMap<>(initialCapacity); 
 }


2. add() 方法也是委托给 LinkedHashMap 来实现的,将元素作为 key 添加到 Map 中

 public boolean add(E e) {
     return map.put(e, PRESENT)==null;
 }


3. clear() 方法清空 LinkedHashSet,就是清空内部 LinkedHashMap

 public void clear() {
     map.clear();
 } 


4. hashCode() 方法和 equals() 方法也都是委托给内部 Map 实现的

 public int hashCode() {
     return map.hashCode();
 }
 
 public boolean equals(Object o) {
     return o == this || 
         (o instanceof LinkedHashSet && 
          map.equals(((LinkedHashSet)o).map));
 }


5. iterator() 方法返回 LinkedHashMap 的 keySet() 遍历器,这样我们在遍历 Set 时,就是按 Map 的顺序遍历 key

 public Iterator<E> iterator() { 
     return map.keySet().iterator(); 
 }


6. 额外再看一下 LinkedHashMap 的部分源码,它继承自 HashMap,并添加了一条双向链表,来维护元素的添加顺序

 public class LinkedHashMap<K,V> extends HashMap<K,V> {
     /**
      * The head of the doubly linked list. 
      */
     private transient LinkedHashMapEntry<K,V> head;
 
     /**
      * The tail of the doubly linked list. 
      */
     private transient LinkedHashMapEntry<K,V> tail;
 
     // ...
     
     public V put(K key, V value) {
         // ... 
         // 将新添加的节点放到双向链表的尾部
         LinkedHashMapEntry<K,V> e = 
             (LinkedHashMapEntry<K,V>)super.newNode(hash, key, value, node);
         connectHeadTail(e);
     }
     
     // 连接双向链表的首尾节点
     private void connectHeadTail(LinkedHashMapEntry<K,V> e) {
         LinkedHashMapEntry<K,V> head = this.head;
         LinkedHashMapEntry<K,V> tail = this.tail;
         e.before = tail;
         e.after = null;
         if (tail != null) {
             tail.after = e;
         }
         this.head = e;
         if (tail == null) {
             this.tail = e;
         }
     }
 }

2.2.3 TreeSet

基于红黑树实现,可以对集合中的元素自动排序。

TreeSet 是一个有序的 Set 集合,它的排序顺序可以自定义比较器确定。如果不提供比较器,默认情况下会按自然排序的顺序排序。

TreeSet 的实现是基于 TreeMap 的,实现原理:

1.  TreeSet 内部包含一个 TreeMap,该 TreeMap 的 key 是添加到 TreeSet 中的元素,value 是默认值(可以是任意值)。

2. 当我们向 TreeSet 中添加元素时,实际上是把元素添加到内部的 TreeMap 中。TreeMap 会根据元素的比较结果,把元素添加到树中的正确位置,以保证 TreeMap 的键值有序。

3. TreeSet 重写了 Set 接口的方法,并委托给内部的 TreeMap 来实现。所以,TreeSet 之所以有序,是因为它内部的 TreeMap 有序。

4. TreeSet 支持自定义比较器。我们可以在构造 TreeSet 时传入比较器,然后 TreeMap 内部就会使用该比较器来维护元素的排序顺序。如果不指定,则默认使用元素的自然排序顺序。

5. TreeSet 不允许集合中出现重复元素,这也是通过内部 TreeMap 的 key 不重复来实现的。

TreeSet 的部分源码解读:

1. 首先看构造方法,TreeSet 委托给 TreeMap 来初始化

 public TreeSet() {
     this(new TreeMap<E,Object>());
 }
 
 public TreeSet(Comparator<? super E> comparator) {
     this(new TreeMap<>(comparator)); 
 }
 
 private TreeSet(NavigableMap<E,Object> m) {
     this.m = m;
 }


2. add() 方法也是委托给内部 TreeMap 来实现的,将元素作为 key 添加到 Map 中

 public boolean add(E e) {
     return m.put(e, PRESENT)==null;
 }


3. clone() 方法也是委托给内部 TreeMap 实现的,将 TreeMap 克隆一份,并设置到新的 TreeSet 中

 public Object clone() {
     TreeSet<E> clone = null;
     try { 
         clone = (TreeSet<E>) super.clone();
     } catch (CloneNotSupportedException e) {
         throw new InternalError(e);
     }
 
     clone.m = new TreeMap<>(m); 
     return clone;
 }  


4. 其他方法比如 contains()、remove()、size() 等也都是委托给内部的 TreeMap 来实现的。

5. TreeMap 通过红黑树的数据结构来维护键值对的有序性和唯一性。添加元素时会调用 compareTo() 或 comparator 来比较元素,并把元素添加到红黑树的正确位置。

6. TreeSet 之所以有序和不重复,就是因为内部使用 TreeMap 来存放元素,并委托大多数方法给 TreeMap 来实现的。

TreeSet 的有序性、唯一性以及其他功能都是通过内部的 TreeMap 来完美实现的。两者的关系是:TreeSet 委托,TreeMap 实现。

2.2.4 红黑树

红黑树是一种自平衡的二叉查找树,它能够保证每个节点的左右子树高度差不超过 1,所以它看起来很平衡。红黑树具有以下几个特征:

1. 每个节点或者是红色的,或者是黑色的。

2. 根节点是黑色的。

3. 所有叶子节点(即 NULL 节点)都是黑色的。

4. 如果一个节点是红色的,那么它的两个子节点一定都是黑色的。

5. 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。

红黑树的这些特征保证了它的关键特性:

1. 添加或删除节点的时间复杂度为 O(logN)。红黑树是近乎平衡的,所以基本能保持二叉查找树的 logN 时间性能。

2. 无需做大的树型调整就能保持平衡,因为每次添加或删除节点后,只需要通过重新着色和旋转来修正树的结构,将它转换回一棵红黑树。

红黑树的实现通常依靠颜色位和链接来代替父链接,这使得它的空间复杂度比普通二叉查找树小一些。

红黑树的一些基本操作:

1. 左旋:将红色节点的右子节点提升为当前节点,并将当前节点设为右子节点的左子节点。

2. 右旋:将红色节点的左子节点提升为当前节点,并将当前节点设为左子节点的右子节点。  

3. 着色:改变节点的颜色来修复违反的规则。

4. 添加节点:采用二叉查找树的添加方式,然后通过旋转和着色来修复红黑树的特征。

5. 删除节点:首先找到要删除的节点,然后通过旋转、着色以及连接空节点来修复红黑树的特征。

这里仅对红黑树的原理和特征作一个简单概述,更详细的解析可以参考《算法》这本书,里面有对红黑树原理和实现的深入分析,对理解 TreeSet 和 TreeMap 的实现也有很大帮助。

2.3 Queue:队列,先入先出

Queue 主要有ArrayBlockingQueue,LinkedBlockingQueue,PriorityQueue,LinkedList,ArrayDeque的实现。下面详细介绍下这几种队列的实现。

2.3.1 ArrayBlockingQueue

基于数组实现的有界阻塞队列,采用锁机制,适合高并发场景。通过一个数组与多个变量的组合,实现了一个阻塞队列。相比于 LinkedBlockingQueue 基于链表实现,ArrayBlockingQueue 基于数组实现的效率会高一些。

ArrayBlockingQueue 是基于数组实现的有界阻塞队列。它的主要属性有:

- items 数组:用于存储队列元素
- takeIndex:下一个要取出元素的索引
- putIndex:下一个要放入元素的索引
- count:队列中的元素个数
- lock:锁,用于控制并发访问,采用ReentrantLock实现

其中,putIndex 和 takeIndex 指示了队列的头和尾,count 表示队列大小。

当我们调用 add() 方法添加元素时,即入队时,如果数组满了,则调用 notFull.await() 等待;如果没满,则 elements[putIndex] 入队,并调整 putIndex 和 count

1. 先获取锁 lock
2. 如果 count == items.length(达到最大容量),则调用 notFull.await() 等待
3. 否则,将元素添加到 items[putIndex] 处,并调整 putIndex 与 count
4. 释放锁 lock,唤醒等待的线程

当我们调用 poll() 取出元素时,即出队时,如果队列空,则返回 null;否则从 elements[takeIndex] 出队,调整 takeIndex 和 count。

1. 先获取锁 lock 
2. 如果 count == 0(队列为空),则返回 null
3. 否则,从 items[takeIndex] 取出元素,调整 takeIndex 与 count
4. 释放锁 lock,唤醒等待的线程
5. 返回取出的元素

ArrayBlockingQueue 通过锁、条件变量以及循环队列或链表的结构实现了线程安全的阻塞添加、阻塞取出、有界等功能。

2.3.2 LinkedBlockingQueue

LinkedBlockingQueue 是基于链表实现的无界阻塞队列。它的主要属性有:

- 链表头节点 head 和尾节点 tail
- 计数器 count 记录队列大小
- 锁 lock 用于控制并发访问
- 条件变量 notEmpty 用于等待队列不空
- 条件变量 notFull 用于等待队列有空间

其中,head 和 tail 指示了队列的头和尾,count 表示队列当前元素个数。

当我们调用 add() 方法添加元素时,即入队操作:

1. 先获取锁 lock
2. 如果条件变量 notFull 等待,直到队列有空间
3. 构造新节点 newNode,节点值设为输入值
4. if head == null(第一次添加),则头尾节点都指向新节点
5. 否则,将 tail 的 next 指向新节点,并将 tail 移动到新节点
6. count加1,唤醒等待的线程
7. 释放锁 lock

当我们调用 poll() 取出元素时,即出队操作:

1. 先获取锁 lock 
2. 如果 head == null(队列空),则返回 null
3. 否则,从 head 节点取出元素
4. 如果 head 的 next 为空,则将 head 和 tail 置为空
5. 否则,将 head 移到 next 节点
6. count减1,唤醒等待的线程 
7. 释放锁 lock
8. 返回取出的元素

LinkedBlockingQueue 通过链表结构实现FIFO的入队和出队操作,并采用ReentrantLock锁与条件变量来实现阻塞功能。相比ArrayBlockingQueue,LinkedBlockingQueue 是无界的,容量仅受内存限制。所以,当需要无界队列或内存消耗不是很高时,LinkedBlockingQueue 是不错的选择。当需要有界队列或考虑内存消耗时,ArrayBlockingQueue 可能更加适合。

2.3.3 PriorityQueue

基于优先级堆实现的无界队列,采用优先级来控制出队顺序,按优先级从小到大出队,非线程安全,不适合高并发场景,用于优先级较高的场景,当我们需要实现一个无界优先级队列且并发不高时,PriorityQueue 是一个很好的选择。

PriorityQueue 的主要属性有:

- 数组 queue 用于存储队列元素
- 容量 capacity 和大小 size
- 比较器 comparator 用于比较元素优先级

其中,queue 数组存储的是优先级比较后的有序数据,capacity 是数组容量,size 是队列当前元素个数。

当我们调用 add() 方法添加元素时,即入队时,先将元素添加到数组末尾,然后上浮到正确位置。

1. 如果需要扩容,则进行扩容,扩为原来的 2 倍
2. 将新元素添加到数组末尾
3. 然后从末尾节点开始上浮,直到找到正确位置
4. 上浮过程中与父节点比较,如果父节点优先级更高则交换,否则停止上浮

当我们调用 poll() 方法出队时,即出队时,返回堆顶元素,然后将最后一个元素替换堆顶,下沉到正确位置。

1. 将队首元素取出返回
2. 用数组最后一个元素替换队首元素
3. 然后从队首开始下沉,直到找到正确位置  
4. 下沉过程中与子节点比较,如果子节点优先级更高则交换,否则停止下沉
5. 最后返回出队元素

PriorityQueue 通过数组与上浮下沉算法实现了一个基于优先级的队列,时间复杂度为 O(logN)。但是需要注意的是,PriorityQueue 不是线程安全的,不适合高并发场景。

PriorityQueue 的主要特点:

1. 基于优先级堆实现,默认最小堆
2. 入队时上浮,出队时下沉
3. 非线程安全,不适合高并发
4. 无界队列,容量只受内存限制
5. 时间复杂度 O(logN)

2.3.4 LinkedList

LinkedList 是基于双向链表实现的队列。它的主要属性有:

- 链表头节点 first 和尾节点 last
- 计数器 size 记录列表大小

其中,first 和 last 分别指向链表的头和尾,size 表示链表当前元素个数。

当我们调用 add(E e) 方法添加元素时,即入队操作时:

1. 构造新节点 newNode,值设为 e
2. 如果链表为空,则 first 和 last 都指向新节点
3. 否则,将 last 的 next 指向新节点,新节点的 prev 指向 last,然后 last 指向新节点
4. size 加 1

当我们调用 remove(E e) 方法删除元素时,即出队操作时:

1. 找到要删除的节点 cur,通过循环遍历链表
2. 如果 cur 为第一个节点,则将 first 指向 cur 的 next
3. 如果 cur 为最后一个节点,则将 last 指向 cur 的 prev 
4. 否则,将 cur 的 prev 的 next 指向 cur 的 next,cur 的 next 的 prev 指向 cur 的 prev
5. size 减 1,然后返回被删除的元素

当我们调用 pollFirst() 头部出队时:

1. 如果链表为空,则返回 null 
2. 否则,获取 first 节点,first 指向 first 的 next
3. 如果出队的是最后一个节点,则 last 也指向 first  
4. size 减 1,返回出队元素  

LinkedList 通过双向链表实现了列表结构,所以添加、删除、遍历等操作都是 O(1) 的时间复杂度。但是缺点是占用内存空间较大,并且不支持随机访问。

所以,当我们需要实现一个动态列表,并且考虑到添加删除频繁时,LinkedList 是一个很好的选择。但如果需要实现随机访问列表或内存空间配合紧张时,ArrayList 可能会更加适合。

2.3.5 ArrayDeque

ArrayDeque是基于双端队列,内部使用数组实现。实现原理:

1. 数组的默认大小是16,如果队列超过默认大小,数组大小会扩展到原来的2倍。这可以通过初始化指定数组大小来避免。

2. 队头和队尾使用两个索引来管理,如果队头到达数组尾部,会将队头重置到数组头部,实现循环使用。

3. addFirst(),addLast(),removeFirst(),removeLast()等方法实现双端插入和删除,时间复杂度为O(1)。

4. 扩容时会将原数组中的元素复制到新数组,这是一个O(n)的操作,所以如果事先知道队列大小,初始化指定大小可以提高效率。

5. 弹出时不会缩容,目的是避免频繁扩容和缩容。

ArrayDeque的部分源码解读:
 

 public ArrayDeque() {
     elements = new Object[16];  //默认大小16 
 }
 
 //头尾索引
 private transient int head;
 private transient int tail;
 
 //扩容方法  
 private void doubleCapacity() {
     assert head == tail;
     int p = head;
     int n = elements.length;
     int r = n - p; 
     int newCapacity = n << 1; //翻倍
     if (newCapacity < 0)   //越界检查
         throw new IllegalStateException("Sorry, deque too big");
     Object[] a = new Object[newCapacity];
     System.arraycopy(elements, p, a, 0, r);  //复制尾部元素
     System.arraycopy(elements, 0, a, r, p);  //复制头部元素
     elements = a;
     head = 0;
     tail = n;
 }
 
 //头部插入方法
 public void addFirst(E e) {
     if (e == null) 
         throw new NullPointerException();
     elements[head = (head - 1) & (elements.length - 1)] = e;
     if (head == tail)
         doubleCapacity();
 }
 
 //尾部插入方法  
 public void addLast(E e) {
     if (e == null)
         throw new NullPointerException();
     elements[tail] = e;
     if ( (tail = (tail + 1) & (elements.length - 1)) == head)
         doubleCapacity();
 }

3.Map接口下的集合

Map是一个映射接口,用于存储键值对映射。主要实现有HashMap、LinkedHashMap、TreeMap等。
 

3.1 HashMap

基于哈希表的Map,无序,键值对不能重复。

实现原理:

1. HashMap底层使用哈希表实现,使用链表处理冲突。JDK1.8之前使用链表,1.8之后使用链表和红黑树结合的方式处理冲突。

2. HashMap的默认初始容量是16,负载因子是0.75。当元素数量达到容量*负载因子时,会进行扩容操作。

3. HashMap使用hashCode()计算键的hash值,然后使用hash值与容量-1做与运算得到桶下标。多个键可能会映射到同一个桶下标,这种情况称为哈希冲突。

4. HashMap会在扩容、插入和删除等操作中重新计算键的hash值,这可能会导致键变换到其他的桶下标,这种现象称为哈希翻转。

5. HashMap的键和值都允许为null。当键为null时,会被映射到table[0]位置。

6. HashMap不保证迭代顺序,这是因为哈希值可能会在操作中改变。如果需要保证顺序,可以使用LinkedHashMap。

7. JDK1.8增加红黑树的数据结构和链表进行结合,当链表长度超过8会将链表转换为红黑树,这可以提高查询效率。

下面是HashMap的部分源码解读:

 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR;  // 默认加载因子0.75
 }
 
 // 扩容方法  
 void resize(int newCapacity) {  
     ...
     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
 }   
 
 //计算hash
 static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
  
 //添加元素   
 public V put(K key, V value) {
     if (key == null)
         return putForNullKey(value); 
     int hash = hash(key);
     int i = indexFor(hash, table.length);
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;
             e.value = value;
             return oldValue;
         }
     }
 
     addEntry(hash, key, value, i);  //添加新entry
     return null;
 }  

3.2 Hashtable

Hashtable 是一个散列表,它存储键值对,和HashMap类似,但是是线程安全的。。

实现原理:

1. 存储结构:Hashtable 使用数组+链表的方式存储数据。它包含一个数组,数组中的每个元素都是一个链表,链表中存储散列到同一个索引的元素。

2. 散列函数:Hashtable 使用键对象的 hashCode() 方法获取该对象的散列码,然后再通过散列码映射到数组的索引上。如果两个键散列到同一个索引,就将其放在同一个链表上。

3. 解决散列冲突:Hashtable 使用链地址法来解决散列冲突,即将散列到同一索引的元素链成一个链表,放在同一个索引下。

4. 扩容:当 Hashtable 的大小超过数组大小*负载因子(load factor)时,Hashtable 会自动扩容,并重新计算每个元素在新的数组中的位置,从而确保散列表的性能。

5. 线程安全:Hashtable 通过使用内部的锁机制来实现线程安全,在任何线程想要读取或写入 Hashtable 的时候,都会先获得锁。这也导致了 Hashtable 的并发性比较差。

6. 键和值:Hashtable 的键和值都可以是任何引用类型的数据。键是一个唯一的标识符,而值则是与这个键相关联的对象。

Hashtable 的典型应用场景就是需要一个线程安全的散列表来存储键值对数据。比如很多的 Java 库都使用 Hashtable 来实现配置信息的存储、缓存等。

3.3 TreeMap

TreeMap 是一个有序的键-值对集合,键值不能重复,它是基于红黑树实现的。实现原理:

1. 存储结构:TreeMap 基于红黑树的数据结构来存储键值对。红黑树是一种自平衡的二叉搜索树,它可以保证任何一个节点的左子树和右子树的高度差不会超过1,从而可以保证查找、插入和删除的时间复杂度为O(logN)。

2. 键的排序:TreeMap 会根据键的自然顺序或者传入的 Comparator 进行排序。这个排序过程发生在插入键值对的时候。

3. 键的唯一性:TreeMap 的键是唯一的。如果插入的键值对的键和已经存在的键相同,那么新的键值对会替换掉已经存在的键值对。

4. 红黑树的插入与删除:在插入和删除时,会通过对红黑树进行颜色翻转、旋转等操作来保持红黑树的平衡,这些操作会由 JDK 内部自动完成。

5. 线程不安全:TreeMap 不是线程安全的,在多线程情况下需要使用 Collections.synchronizedSortedMap() 方法获得一个线程安全的 TreeMap。

6. 键和值:TreeMap 的键和值都可以是任何引用类型的数据。且 TreeMap 会根据键的排序来决定值的存储顺序。

所以 TreeMap 的典型应用场景是需要一个有序的键值对集合来存储数据,并且对键的排序和快速查找有需求。比如很多集合框架内部使用 TreeMap 来实现 SortedSet 和 SortedMap 接口。

3.4 LinkedHashMap

有序的HashMap,基于链表和哈希表实现,迭代顺序为元素的插入顺序。

可以说LinkedHashMap 是一个有序的 HashMap。它继承自 HashMap,其底层仍然是基于拉链法实现的 HashMap,只是额外维护了一个双向链表,使得键值对有顺序。

LinkedHashMap 的结构如下:

 public class LinkedHashMap<K,V> extends HashMap<K,V> {
     private 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 的节点 Entry 继承自 HashMap 的节点 Node,并额外维护了 before 和 after 指针,用于构建双向链表。

在 LinkedHashMap 中,当我们把键值对放入链表时,都会插入到双向链表的尾部。在发生 Hash 冲突时,会按照键值对的插入顺序来维护链表,从而保证了有序性。

另外,LinkedHashMap 还提供了访问顺序的选项:

- accessOrder = true:最近最少使用原则,get 或 put 一个节点时会将其移动到链表尾部,保证最近访问的节点靠近链表尾部。
- accessOrder = false:插入顺序,新添加的节点总是插入到链表尾部。

所以简单来说,LinkedHashMap 通过维护一个双向链表,使得 HashMap 有了顺序,从而实现了一个有序的 HashMap。

4.Iterator

很多Java集合中都用到了Iterator,Iterator接口实现了迭代器设计模式,它提供一种方法访问集合对象中的元素,而又不需要暴露该对象的内部实现。

Iterator 接口有两个主要方法:

- hasNext():返回集合中是否还有元素
- next():返回集合中的下一个元素,并移动游标到下一个元素

Iterator 的实现会有一个游标来记录遍历的位置,每次调用 next() 方法,游标就会向下移动。可通过remove()方法删除迭代器刚遍历过的元素。

我们可以看看 ArrayList 的 Iterator 实现来理解其原理。

ArrayList 维护一个数组来存储元素,当创建一个 Iterator 时,它会将当前游标位置传给 Iterator:

 

 public Iterator<E> iterator() {
     return new Itr();
 }
 
 private class Itr implements Iterator<E> {
     int cursor;       // 遍历的位置游标
     int lastRet = -1; // 上一个返回元素的索引 
 
     // ...
 } 


在 Iterator 的 hasNext() 和 next() 方法中,就直接操作这个 cursor 来遍历数组:

 public boolean hasNext() {
     return cursor != size;
 }
 
 public E next() {
     checkForComodification();
     int i = cursor;
     cursor = i + 1;
     return array[lastRet = i];
 }


Iterator 的实现就是依赖于一个游标来记录遍历位置,并在 hasNext() 和 next() 方法中移动这个游标,从而实现迭代效果。

5.集合间的转换

Java中的集合框架提供了各种集合类型,他们之间可以相互转换,掌握好Java 集合可以大大提高编程效率。。主要的转换方法有:

5.1 List <-> Set

可以使用List.addAll()和Set.addAll()方法进行转换。

 List<String> list = Arrays.asList("a", "b", "c");
 Set<String> set = new HashSet<>(list); 
 
 Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c"));
 List<String> list = new ArrayList<>(set);


5.2 List <-> Queue

可以使用List.addAll()和Queue.addAll()方法进行转换。

 List<String> list = Arrays.asList("a", "b", "c");
 Queue<String> queue = new LinkedList<>(list);  
 
 Queue<String> queue = new LinkedList<>(Arrays.asList("a", "b", "c"));
 List<String> list = new ArrayList<>(queue);   


5.3 Set <-> SortedSet

可以使用Set.addAll()和SortedSet.addAll()方法进行转换。

 Set<String> set = new HashSet<>(Arrays.asList("c", "b", "a"));
 SortedSet<String> sortedSet = new TreeSet<>(set);
 
 SortedSet<String> sortedSet = new TreeSet<>(Arrays.asList("c", "b", "a")); 
 Set<String> set = new HashSet<>(sortedSet);


5.4 List <-> Map

可以使用Map.putAll()方法将List的值作为Map的键或值进行转换。

 List<String> list = Arrays.asList("a", "b", "c"); 
 Map<String, Integer> map = new HashMap<>();
 for (int i = 0; i < list.size(); i++) {
     map.put(list.get(i), i); 
 }
 
 Map<String, Integer> map = new HashMap<>();
 map.put("a", 0);
 map.put("b", 1); 
 map.put("c", 2);
 List<String> list = new ArrayList<>(map.keySet()); 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

撸码猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值