Java集合大全

集合的迭代器

目前Java里有三个迭代器:Iterator Iterable ListIterator。

  1. 首先看Iterator接口:

public interface Iterator {
boolean hasNext();
E next();
void remove();
}
提供的API接口含义如下:
hasNext():判断集合中是否还有下一个对象。
next():返回集合中的下一个对象,并将访问指针移动一位。
remove():删除集合中调用next()方法返回的对象。
在JDK早期版本里,遍历集合的方式只有一种,那就是通过Iterator迭代器操作,具体实例如下:

复制代码
List list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer next = iter.next();
System.out.println(next);
if (next == 20) {
iter.remove();
}
}
复制代码
2. Iterable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Iterable {
Iterator iterator();
// since JDK 1.8
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// since JDK 1.8
default Spliterator spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
 Iterable接口里面提供了一个Iterator()方法返回迭代器,因此实现了Iterable接口的集合依旧可以使用迭代器遍历和操作集合中的对象。在 JDK 1.8中,Iterable接口新增了一个方法forEach(),它允许使用增强 for 循环遍历对象。

为什么要设计两个接口Iterable和Iterator?Iterator接口的保留可以让子类去实现自己的迭代器,而Iterable接口更加关注于for-each的增强语法。
  1. ListIterator

ListIterator继承 Iterator 接口,仅存在于 List 集合之中,通过调用方法可以返回起始下标为 index的迭代器。ListIterator 中有几个重要方法,大多数方法与 Iterator 中定义的含义相同,此外根据返回的迭代器,且可以实现双向遍历。

1
2
3
4
5
6
7
8
9
10
11
12
public interface ListIterator extends Iterator {
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void remove();
// 替换当前下标的元素,即访问过的最后一个元素
void set(E e);
void add(E e);
}
三、Map和Collection接口

Map接口和Collection接口是Java的集合框架中的两个重要门派,Collection存储的是集合元素本身,Map存储的是<key,value>键值对,但是部分Collection子类使用了Map来实现的。例如:HashSet底层使用了HashMap,TreeSet底层使用了TreeMap,LinkedHashSet底层使用了LinkedHashMap

Map接口的数据结构是<key, value>形式,key 映射value,一个key对应一个value,key不可重复,value可重复。Map接口细分为不同的种类:

SortedMap接口:该类映射可以对<key, value>按照自己的规则进行排序,具体实现有 TreeMap
AbsractMap抽象类:它为子类提供好一些通用的API实现,所有的具体Map如HashMap都会继承它

Collection接口提供了所有集合的通用方法(注意这里不包括Map):
添加方法:add(E e) / addAll(Collection<? extends E> var1)/ addAll(int index, Collection<? extends E> var1)
查找方法:contains(Object var1) / containsAll(Collection<?> var1)/
查询集合自身信息:size() / isEmpty()
删除方法:remove(O o)/removeAll(Collection<?> var1)/求交集:retainAll(Collection c)

... ...

Collection接口将集合细分为不同的种类:

Set接口:一个不允许存储重复元素的无序集合,具体实现有HashSet / TreeSet···
List接口:一个可存储重复元素的有序集合,具体实现有ArrayList / LinkedList···
Queue接口:一个可存储重复元素的队列,具体实现有PriorityQueue / ArrayDeque···
  1. Map接口详解
    Map 体系下主要分为 AbstractMap 和 SortedMap两类集合
    AbstractMap是对Map接口的扩展,定义了普通Map集合具有的通用方法,可避免子类重复编写大量相同代码,子类继承 AbstractMap 后可重写它的方法,并实现额外逻辑,对外可提供更多的功能。
    SortedMap接口定义了Map具有排序行为,当子类实现它时,必须重写所有方法,对外提供排序功能。

1.1 HashMap
HashMap 是一个最通用的利用哈希表存储元素的集合,有元素加入HashMap时,会将key的哈希值转换为数组的索引下标确定存放位置,在查找元素时,根据key的哈希地址转换成数组的索引下标确定查找位置。
HashMap 底层是用数组 + 链表 + 红黑树这三种数据结构实现,是非线程安全的集合。

加入元素发生哈希冲突时,HashMap会将相同地址的元素连成一条链表,若链表长度大于8,且数组长度大于64会转换成红黑树数据结构。

关于 HashMap 的简要总结:
1. 它是集合中最常用的Map集合类型,底层由数组 + 链表 + 红黑树组成;
2. HashMap不是线程安全的;
3. 插入元素时,通过计算元素哈希值,通过哈希映射函数转换为数组下标;查找元素时,通过哈希映射函数得到数组下标定位元素的位置。

1.2 LinkedHashMap
LinkedHashMap可以看作是 HashMap 和 LinkedList 的结合:它是在 HashMap 的基础上添加了一条双向链表,默认存储各个元素的插入顺序,但由于这条双向链表,使得 LinkedHashMap 可以实现 LRU缓存淘汰策略,因为我们可以设置这条双向链表按照元素的访问次序进行排序

1
2
3
4
// 头节点
transient LinkedHashMap.Entry<K, V> head;
// 尾结点
transient LinkedHashMap.Entry<K, V> tail;
利用 LinkedHashMap 可以实现 LRU 缓存淘汰策略,因为它提供了一个方法:

1
2
3
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return false;
}
该方法可以移除最靠近链表头部的一个节点,而在get(key)方法源代码如下,其作用是挪动结点的位置:

1
2
3
4
5
6
7
8
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
只要调用了get(key)且accessOrder = true,则会将该节点更新到链表尾部,具体的逻辑在afterNodeAccess()中,源码如下:

复制代码
void afterNodeAccess(Node<K,V> e) { // move node to last
  LinkedHashMap.Entry<K,V> last;
  if (accessOrder && (last = tail) != e) {
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.after = null;
    if (b == null) head = a;
     else b.after = a;
    if (a != null) a.before = b;
    else last = b;
    if (last == null) head = p;
    else {
      p.before = last;
      last.after = p;
    }
    tail = p;
    ++modCount;
  }
}
复制代码
指定accessOrder = true,可以设定链表按照访问顺序排列,通过提供的构造器可以设定accessOrder。

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
重写removeEldestEntry()方法,内部定义逻辑,通常是判断容量是否达到上限,若是则执行淘汰。

关于 LinkedHashMap 总结两点:
1. 底层维护了一条双向链表,继承了 HashMap,所以不是线程安全的
2. LinkedHashMap 可实现LRU缓存淘汰策略,其原理是通过设置accessOrder为true并重写removeEldestEntry方法定义淘汰元素时需满足的条件。

1.3 TreeMap
TreeMap 是 SortedMap 的子类,所以它具有排序功能,是基于红黑树数据实现的,每个键值对<key, value>都是一个结点,默认情况下按照key自然排序,另一种是可以通过传入定制的Comparator进行自定义规则排序。

// 按照 key 自然排序,Integer 的自然排序是升序
TreeMap<Integer, Object> naturalSort = new TreeMap<>();
// 定制排序,按照 key 降序排序
TreeMap<Integer, Object> customSort = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));

图中红黑树的每一个节点都是一个Entry,在这里不标明 key 和 value 了,这些元素都是已按照key排好序了,整个数据结构都是保持着有序状态!
关于自然排序与定制排序:
自然排序:要求key必须实现Comparable接口。
由于Integer类实现了Comparable 接口,按照自然排序规则是按照key从小到大排序。

TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, “贰”);
treeMap.put(1, “壹”);
System.out.print(treeMap);
// {1=壹, 2=贰}
定制排序:在初始化 TreeMap 时传入新的Comparator,不要求key实现 Comparable 接口

复制代码
TreeMap<Integer, String> treeMap = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));
treeMap.put(1, “壹”);
treeMap.put(2, “贰”);
treeMap.put(4, “肆”);
treeMap.put(3, “叁”);
System.out.println(treeMap);
// {4=肆, 3=叁, 2=贰, 1=壹}
复制代码
关于 TreeMap 主要介绍了三点:
1. 它底层是由红黑树实现的,操作的时间复杂度为O(logN);
2. TreeMap 可以对key进行自然排序或者自定义排序,自定义排序时需要传入Comparator,而自然排序要求key实现了Comparable接口;
3. TreeMap 不是线程安全的。

1.4 WeakHashMap
WeakHashMap底层存储的元素的数据结构是数组 + 链表,没有红黑树。日常开发中用的很少,是基于Map实现,Entry中键在每一次垃圾回收都会被清除,适合用于短暂访问或访问一次的元素,缓存在WeakHashMap中,并尽早地把它回收掉。

public class WeakHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {
}
  当Entry被GC时,WeakHashMap 是如何感知到某个元素被回收的呢?

    在 WeakHashMap 内部维护了一个引用队列queue:

private final ReferenceQueue queue = new ReferenceQueue<>();
queue里包含了所有被GC掉的key,当JVM开启GC后,如果回收掉WeakHashMap中的key,会将key放入queue 中,在expungeStaleEntries()中遍历 queue,把queue中的所有key拿出来,并在WeakHashMap中删除掉,以达到同步。

图中被虚线标识的元素将会在下一次访问 WeakHashMap 时被删除,WeakHashMap 内部会做好一系列的调整工作,所以队列的作用是标志已经被GC掉的元素。
关于 WeakHashMap 需要注意三点:
1. 它的键是一种弱键,放入 WeakHashMap 时,随时会被回收掉,所以不能确保某次访问元素一定存在;
2. 它依赖普通的Map进行实现,是一个非线程安全的集合;
3. WeakHashMap 通常作为缓存使用,适合存储那些只需访问一次、或只需保存短暂时间的键值对。

1.5 HashTable
Hashtable底层存储结构是数组+链表,是一个线程安全的集合。当链表过长时,查询效率过低,会长时间锁住Hashtable,所以在并发环境下,性能很差,现在基本上被淘汰了,也很少用了。线程安全,它所有的方法都被加上了 synchronized 关键字。HashTable 默认长度为 11,负载因子为 0.75F,即元素个数达到数组长度的 75% 时,会进行一次扩容,每次扩容为原来数组长度的 2 倍

  1. Collection 集合体系详解
    Collection 集合体系的顶层接口就是Collection,它规定了该集合下的一系列方法。
    该集合下可以分为三大类集合:List,Set和Queue
    Set接口:不可存储重复的元素,且任何操作均需要通过哈希函数映射到集合内部定位元素,集合内部元素默认无序。
    List接口:可存储重复的元素,且集合内部的元素按照元素插入的顺序有序排列,可以通过索引访问元素。
    Queue接口:是以队列作为存储结构,集合内部的元素有序排列,仅可以操作头结点元素,无法访问队列中间的元素。
    上面三个接口是最普通,最抽象的实现,而在各个集合接口内部,还会有更加具体的表现,衍生出各种不同的额外功能,使开发者能够对比各个集合的优势,择优使用。

2.1 Set接口
Set接口继承了Collection接口,是一个不包括重复元素的集合,更确切地说,Set 中任意两个元素不会出现 o1.equals(o2),而且 Set 至多只能存储一个 NULL 值元素。

在Set集合体系中,我们需要着重关注两点:
存入可变元素时,必须非常小心,因为任意时候元素状态的改变都有不可能使得 Set 内部出现两个相等的元素,即 o1.equals(o2) = true,所以一般不要更改存入 Set 中的元素,否则将会破坏了 equals() 的作用!
Set 的最大作用就是判重,在项目中最大的作用也是判重!

HashSet
HashSet 底层借助 HashMap 实现,我们可以观察它的多个构造方法,本质上都是 new 一个 HashMap

复制代码
public class HashSet extends AbstractSet implements Set, Cloneable, Serializable {
public HashSet() {
this.map = new HashMap();
}
public HashSet(int initialCapacity, float loadFactor) {
this.map = new HashMap(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
this.map = new HashMap(initialCapacity);
}
}
复制代码
我们可以观察add()方法和remove()方法是如何将 HashSet 的操作嫁接到 HashMap 的。

复制代码
private static final Object PRESENT = new Object();
public boolean add(E e) {
return this.map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return this.map.remove(o) == PRESENT;
}
复制代码
HashMap 的 value 值,使用HashSet的开发者只需关注于需要插入的 key,屏蔽了 HashMap 的 value

HashSet 在 HashMap 基础上实现,所以很多地方可以联系到 HashMap:
底层数据结构:HashSet 也是采用数组 + 链表 + 红黑树实现
线程安全性:由于采用 HashMap 实现,而 HashMap 本身线程不安全,在HashSet 中没有添加额外的同步策略,所以 HashSet 也线程不安全
存入 HashSet 的对象的状态最好不要发生变化,因为有可能改变状态后,在集合内部出现两个元素o1.equals(o2),破坏了 equals()的语义。

LinkedHashSet
LinkedHashSet 的代码少的可怜,如下:

复制代码
public class LinkedHashSet
extends HashSet
implements Set, Cloneable, java.io.Serializable {

private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
    super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
}
@Override
public Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}

}
亚马逊测评 www.yisuping.com

猜你喜欢

转载自blog.csdn.net/weixin_45032957/article/details/108576533