Java集合框架
集合接口与实现分离
Java集合类库也将接口与实现分离。
Collection接口
在Java类库中,集合类的基本接口是Collection
接口。这个接口有两个基本方法:
boolean add(E element);
Iterator<E> ietrator();
迭代器
Iterator接口包含4个方法:
1 | public interface Iterator<E>{ |
可以通过反复调用next方法,可以逐个访问集合中的每个元素。
但是如果达到了集合的末尾,next方法将抛出一个NoSuchElementException
。所以可以在调用next方法之前调用hasNext方法来判定是否有一下个元素。
如:
1 | Collection<String> c = ...; |
但是在jdk5中引入了增强for循环:
1 | Collection<String> c = ...; |
编译器简单地将for each
循环转换为带有迭代器的循环。增强for循环可以处理任何带有低迭代器的循环。
使用forEachRemaining
来便利。
1 | Collection<String> c = ...; |
Iteratot的next方法和hasNext方法与Enumeration接口的nextElement和hasMoreElements方法的作用一样。而Iterator接口的设计初衷就是为了用更短的名称代替它。
其实迭代器的方式与流的读取类似。每用一次都会消耗掉一个元素。
泛型使用方法
由于Collection
与Iterator
都是泛型接口,这意味着可以编写处理任何集合类型的使用方法。
Collection
接口内部定义很多实用的方法。如:
int size();
boolean isEmpty();
blooean contains();
boolean equals();
- …
但是由于每个子类都要实现这些方法,所以又定义了AbstractCollection
抽象类。其保持了那些基础的方法,比如size
和Iterator
。但是实现了其他的例行方法。
这样具体类可以扩展AbstractCollection
类。
集合框架中的接口
Java集合框架为不同类型的集合定义了大量接口。如下
其主要有两个基本接口,Collection
和Map
。其子结构的介绍如下
Collection
:表示单个值的集合。List
: 是一个有序集合。元素会增加到指定的特定位置。Set
: 无序但其内部元素不允许重复。Queue
:队列。
Map
:表示键值对的集合。
具体集合
下面的表格简单介绍了Java库的一部分具体集合
接口类型 | 描述 |
---|---|
ArrayList |
可以动态增加和缩减的一个索引序列。 |
LinkedList |
可以在任何位置高效插入和删除的一个有序序列。 |
ArrayDeque |
实现为循环数组的一个双端队列。(Deque->Queue->Collection) |
Stack |
一个栈的实现。(List) |
HashSet |
没有重复元素的一个无序集合。 |
TreeSet |
一个有序树集。 |
EnumSet |
一个包含枚举类型值的集合。 |
LinkedHashSet |
一个可以记住元素插入次序的集合。(HashSet,->Set) |
PriorityQueue |
允许高效删除最小元素的一个集合。(AbstractQueue->Queue->Collection) |
HashMap |
存储键/值对的一个数据结构。 |
TreeMap |
键有序的一个映射。 |
EnumMap |
键属于枚举类型的一个映射。 |
LinkedHashMap |
可以记住键值对添加次序的一个映射。 |
WeakHashMap |
值不在别处使用时就可以被垃圾回收的一个映射。 |
IdentityHashMap |
用==而不是equls比较键的一个映射。(即比较地址,并不是相等) |
链表(LinkedList
)
链表的实现与ArrayList
不同,其是由节点一个个连接起来的链。所以其特点也与数组列表不同。
具体链表和数组的为什么会出现这些特点不再赘述,基本知识。其知识如下:
- 链表插入和删除更快。
- 链表不能直接按下标查找值,需要遍历。
数组列表(ArrayList
)
这个类的底层是用数组实现的,因此可以直接实现按下表查找值。
注意:vector
也可以实现动态数组,但是vector是同步的,所以在不需要多线程访问的时候,即不需要同步的时候,选择ArrayList
效率更高。因为其不需要同步。(vector是老版本的结构,建议使用新的ArrayList)
散列集(HashSet
)
HashSet
是利用了hash table的原理,具体原理也是基础知识,不再赘述。
其主要特点是:
- 没有顺序。
- 不会存在重复的值。
树集(TreeSet
)
TreeSet类与散列集十分相似,元素不允许重复。
不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。
可以将任意顺序的元素插入到集合中。在对集合进行遍历的时候,值将自动按照排序后的顺序呈现。
其底层实现是红黑树。
每将一个元素添加到树中时,都会将其放置到正确的位置。因此迭代器总会以有序的访问每个元素。
将一个元素添加到树中总要比添加到散列表中慢。
所以到底是选用散列表还是树集要根据需求,如果不要求数据有序,则选用散列表。但是如果元素过多,则速度会很慢。
队列(Queue
)
队列是一个先进先出的集合。其本身是一个接口。定义了需要的方法。
1 | boolean add(E e); |
双端队列
双端队列(即Deuqe)允许在头部和尾部都高效地添加或删除元素。Java6引入了Deque接口。
其中一部分接口:
void addFirst(E e);
void addLast(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getLast();
E peekFirst();
E peekLast();
int size();
Iterator<E> iterator();
ArrayDeque
和LinkedList
类实现了这个接口。
数组双端队列(ArrayDeque
)
在import java.util.ArrayDeque;
中定义了ArrayDeque
。
ArrayDeque
实现了Deque
中的所有方法。并且其内部是一个数组来存储数据:
1 | transient Object[] elements; |
由于数组不能增加长度的问题,其内部又定义了一些方法来实现动态增加长度:
1 | /** |
优先队列
优先队列(priority queue)中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。且特点就是:
无论何时调用remove()
方法,总会获得当前优先队列中最小的元素。
优先队列其内部是采用了堆来获取最小值。其原理是DS基础,不再赘述。
这个方法也实现了Comparable
接口,方便在堆中比较大小。
其也是采用了数组存储:
1 | transient Object[] queue; // non-private to simplify nested class access |
整个remove()
的调用栈如下:
remove()-Abstract
->pool()-PriorityQueue
->siftDownUsingComparator()-PriorityQueue
其中pool
是移除队列顶的元素,siftDownUsingComparator
是将x插入后排好顺序。
值得注意的是:优先队列也支持特定元素出队列。即remove(E e)
方法。
映射
映射map
数据结构是为了存储键值对。可以通过键来查找值。
其特点就是键不能重复,值可以重复。
Java底层定义了Map
接口用于统一映射的操作,抽象类AbstractMap
是一个实现了Map
接口的抽象类。
而HashMap
和TreeMap
都实现了 Map
接口并且继承了AbstractMap
类。
映射Map
Map
提供了一下方法定义:
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
void clear();
而其中又定义了
Entry
这种数据接口用于保存单个键值对的子接口,其内部又定义了一些方法:
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
- …
顺序映射SortedMap
SortedMap
是一个继承了Map
的接口。其主要目的是提供一个根据key排序的映射。其内部仅仅提供了一些函数定义(如获取key
的Set
,获取value
的集合),并没有给出默认实现。
导航映射NavigableMap
NavigableMap
是一个继承了SortedMap
的接口。这个抽象类的目的是提供一些用来获取key或value的最大/最小值的方法申明。
比如:
Map.Entry<K,V> lowerEntry(K key);
:返回按照key排序的最大的那个小于给定key的Entry
。K lowerKey(K key);
:返回按照key排序的最大的那个最小于给定key的key
。Map.Entry<K,V> floorEntry(K key);
:返回按照key排序的最大的那个小于或等于给定key的Entry
。K floorKey(K key);
:返回按照key排序的最大的那个小于或等于给定key的key
。Map.Entry<K,V> ceilingEntry(K key);
:返回按照key排序的最小的那个大于给定key的Entry
。K ceilingKey(K key);
: …Map.Entry<K,V> higherEntry(K key);
: 返回按照key排序的最小的那个大于给定key的Entry
。
举个例子:
1 | { |
当给点key为-4时:
- lowerEntry: {3: 3}
- floorEntry: {4: 4}
- ceilingEntry: {4, 4}
- higherEntry: {5: 5}
返回key的规则也一样。
抽象映射AbstractMap
与Set
类似,Map
也定义了一个实现了Map
接口的抽象类AbstractMap
。
其内部为Map
的方法提供了一些默认实现,比如get
方法
1 | public V get(Object key) { |
其利用了Iterator
和Entry
来为其提供了一个通用的实现。因为Map
的数据总会为元素提供这两个实现。
而一些方法则没有实现,需要具体类自己去实现,比如put
。
1 | public V put(K key, V value) { |
散列映射HashMap
其内部定义了Node
实现了原Map
中的Entry
子接口。
其基本实现如下:
1 | static class Node<K,V> implements Map.Entry<K,V> { |
也就是说HashMap
实际上也是由一个个的Node类的实例数组组成的。
其存储在table
中,其初始化函数如下:
1 | /** |
而HashMap
的默认表长为:
1 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 |
装填因子为:
1 | /** |
树映射TreeMap
TreeMap
实现了AbstractMap
,并且实现了NavigableMap
。
所以也实现了上面NavigableMap
的所有方法,例如:
1 | /** |
可以看到这里采用了中序递归遍历的方式来查找等于这个值得node,然后返回其parent(这里有个判定其左子树是否存在过程)。
注意这里TreeMap
中,重写了Entry
:
1 | static final class Entry<K,V> implements Map.Entry<K,V> { |
即这里除了定义了k,v还定义了left
和right
来作为树节点的左右节点。 而这里的树是一种特殊的平衡二叉树-红黑树。DS基础不再赘述。
映射集合
集合框架不认为映射映射本身是一个集合。不过,可以得到得映射的视图。
有3种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值可以构成一个集合,因为映射中一个键只能有一个副本。下面是方法:
Set<K>KeySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
需要说明的是,KeySet
不是HashSet
或 TreeSet
,而是实现了Set
的另外某个类的对象。
tips:可以用var
来代替Map.Entry
这种复杂的声明方式。(尤其在增强for循环中)
弱散列映射
弱散列映射意在解决这样一个问题:如果一个散列映射中,如果一个键已经不在任何地方引用,那么理论上来说,这个映射就无法被任何地方获取,那么它仍然会在jvm中存在,并且不会被垃圾回收。这样显然是内存浪费。
而WeakHashMap
的设计就是为了解决这个问题,通过WeakHashMap
定义的键值对,只要键不在任何地方引用,则一定时间后,这个键值对就会被垃圾回收。
WeakHashMap
使用的是弱引用(weak reference)保存键。WeakReference对象将包含另一个对象的引用,此处就是一个散列表键。
对于这种类型的对象,垃圾回收器采用了一种特殊的方式进行处理。正常情况下,如果垃圾回收器发现某个特点的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器也会将其回收。
这个回收过程是现将这个对象的弱引用放入一个队列。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。
一个弱引用进入队列意味着这个键不再被他人使用,并且已经回收。于是,WeakHashMap将删除相关联的映射条目。
链接散列集与映射
LinkedHashSet
、LinkedHashMap
与HashSet
、HashMap
基本操作一样,只是其实现用了链表的方式。
所以在需要频繁增删元素时选用链表比较合适,否则直接使用数组形式更加方法。
枚举集与映射
EnumSet
是一个枚举类型元素集的高效实现。
由于枚举类型只有有限个实例,所以EnumSet
内部用位序列实现。如果对应的值在集中,
EnumSet
没有公共的构造器。要使用静态工厂方法构造这个集:
1 | enum WeekDay{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SAUNDAY}; |
EnumMap
是一个键类型为枚举类型的映射。其可以直接且高效地实现为一个值数组。
需要在构造器中指定键类型:
1 | var personIncharge = new EnumMap<Weekday, Employee>(Weekday.class); |
标识散列映射(IdentityHahMap
)
IdentityHahMap
有特殊的用途。在这个类中,键的散列值不是用hashCode
函数计算的,而是用System.identityHashCode
方法计算。这是Object.hashCode
根据对象的内存地址计算散列码时所使用的的方法。而且,在对两个对象进行比较时,IdentityHahMap
类使用==
, 而不使用equals
。
也就是说,不同的键对象即使内容相同,也被视为不同的对象。在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪哪些对象已经遍历过。
视图和包装器
视图就是集合或者映射中某一部分或者某一类数据的再映射得到的结果集,这个结果集一般不允许更新(有些视图允许更新某个元素,但是不允许新增或者删除元素),只允许读取,结果集中的数据使用的还是原来集合或者映射中的数据的引用。
小集合
Java9引入了一些静态方法,可以生成给定元素的集或列表,以及给定键/值对的映射。
如:
1 | List<String> names = List.of("Perter", "Paul", "Mary"); |
会生成包含3个元素的一个列表和一个集。
1 | Map<String, Integer> sources = Map.of("Peter", 2, "Paul", 3, "Mary", 4); |
元素、键或值不能为null。
这些方法返回的视图都为ImmutableCollections
的相关子类:
static abstract class AbstractImmutableCollection<E> extends AbstractCollection<E>
:所有内部类的父类。static final class List12<E> extends AbstractImmutableList<E>
: 含有一个或多个的List。static final class ListN<E> extends AbstractImmutableList<E>
:含有多个元素的List。static abstract class AbstractImmutableSet<E> extends AbstractImmutableCollection<E>
: 所有内部Set的父类。static final class Set12<E> extends AbstractImmutableSet<E>
: 含有一个或两个元素的Set。static final class SetN<E> extends AbstractImmutableSet<E>
: 含有多个元素的Set。abstract static class AbstractImmutableMap<K,V> extends AbstractMap<K,V> implements Serializable
:所有内部Map类的父类。static final class Map1<K,V> extends AbstractImmutableMap<K,V>
: 含有一个或多个的Map。static final class MapN<K,V> extends AbstractImmutableMap<K,V>
: 含有多个元素的Map。
值得注意的是:
- 这些内部类的实例无法改变。(尝试改变会抛出一个
UnsupportedOperation Exception
)。 Collection
类包含很多实用方法,这些方法的参数和返回值都是集合。不要将它与Collection
接口混淆。
子范围
可以为很多集合建立子范围(subrange)视图。比如
List
的subList
方法。SortedSet<E> subSet(E from, E to);
SortedSet<E> headSet<E to>;
SortedSet<E> tailSet(E from );
SortedMap<K, V> subSet(K from, K to);
SortedMap<K, V> headSet<K to>;
SortedMap<K, V> tailSet(K from );
NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive);
NavigableSet<E> headSet<E to, boolean toInclusive>;
NavigableSet<E> tailSet(E from, blooean fromInclusive);
以上方法返回截取确定的集合,然后返回对应内容。
不可修改视图
Colelctions
工具类(注意不是Collection
接口)还有几个方法,可以生成集合的不可修改视图(unmodifiable view)。这些视图对现有集合增加了一个运行时检查。试图对其进行修改时会抛出错误,且集合不会改变。
以下8个方法可以获得不修改的视图:
Collections.unmodifiable
CollectionCollections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiable
SortedSetCollections. unmodifiableNavigable
SetCollections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap
每个方法都定义处理一个接口。如:
CollectionCollections.unmodifiableList
处理ArrayList
、LinkedList
或者任何实现了List
接口的类。
同步视图
如果从多个线程访问集合,就必须保证集合不会被意外破坏。例如散列表不是同步的,多个线程同时访问会出现错误。这就是灾难性的。所以,同步中设计了很多线程安全集合。例如Collections.synchronizedMap
就是一个线程安全的Map,使用方法相同,具体详见以后的多线程介绍。
检查性视图
“检查型”视图是用来对泛型可能出现的问题提供调试支持。例如:
1 | var strings = new ArrayList<String>(); |
这个错误的命令在运行时检测不到。实际上,只有当另一部分代码调用get方法,并且将这个结果强制转换为String时,才会出现一个类的强制转换异常。
而检查型视图可以探测这个问题。如:
1 | List<Strting> safeStrings = Collections.checkedList(strings, String.class); |
这个视图的add方法将检查插入的对象是否属于给定的类。如果不是,就会抛出转换错误。如:
1 | ArrayList rawList = safeStrings; |
算法
泛型的优点
泛型集合接口有一个很大的优点,即算法只需实现一次。
排序和混排
Arrays.sort
采用的是快排的方式。- 而对于链表的排序,Java采用的是先将其复制到一个数组中,经过排序再复制回链表中。
二分查找
Collections.binarySearch
是一个二分查找的实现。
集合与数组的转换
由于Java平台API的大部分内容都是在集合框架创建之前设计的,所以,有时候需要在传统的数组和更加现代的集合之间进行转换。
数组转集合
list.of
包装器可以达到这个目的。
1 | String[] values = ...; |
集合转数组
可以使用toArray
方法。
1 | Object[] values = staff.toArray(); |
但是注意这个方法只能返回Object[]
,并且不能强制转换。
遗留的集合
从Java第一版本以来,在集合框架中已经存在大量的“遗留的”容器类。
Hashtable
类
经典的Hashtable
类与HashMap
类作用一样。实际上接口也基本相同。与Vector
类的方法一样,Hashtable
方法也是同步的。如果对遗留代码的兼容性没有需求,则应当使用HashMap
。
如果对并发有需求,则应当使用ConcurrentHashMap
。
枚举
遗留的集合使用Enumeration
接口遍历元素序列。其hasMoreElements
和nextElement
与Iterator
接口的hasNext
和next
方法作用一致。以后应该都使用Iterator
。
属性映射
属性映射(property map
)是一个特殊类型的映射结构。其有以下特点:
- 键和值都是字符串。
- 这个映射可以很容易地保存到文件以及从文件中加载。
- 有一个二级表存放默认值。
在Java平台中类名为properties
。 一般可以用于加载xxx.proprties
文件。
位集
Java平台的BitSet
类主要存储一个未序列(它不是数学上的集,如果称为位向量或位组更为合适)。如果需要高效的存储位序列(例如,标志),就可以使用位集。由于位集合包装在字节里,因此位集要比使用boolean对象的ArrayList高效很多。
例如:
1 | BitSet bSet = new BitSet(); |
可以对第n位调用set
方法。此时第n位状态就为 “开”。
这时再调用get
方法就会返回true
,否则返回false
。