8.6.1 Java 8为Map新增的方法

Java 8除为Map增加了remove(Object key,Object value)默认方法之外,还增加了如下方法。

compute方法

方法 描述
default V compute(K key, BiFunction<? super K,​? super V,​? extends V> remappingFunction) Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current mapping).

该方法使用remappingFunction根据原key-value对计算一个新value

  • 只要新value不为null,就使用新value覆盖原 value;
  • 如果原value不为null,但新valuenull,则删除原key-value对;
  • 如果原value、新value同时为null,那么该方法不改变任何key-value对,直接返回null

computeIfAbsent方法

方法 描述
default V computeIfAbsent(K key, Function<? super K,​? extends V> mappingFunction) If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.

如果传给该方法的key参数在Map中对应的valuenull,则使用mappingFunction根据key计算一个新的结果.

  • 如果计算结果不为null,则用计算结果覆盖原有的value
  • 如果原Map原来不包括该key,那么该方法可能会添加一组key-value对。

computeIfPresent方法

方法 描述
default V computeIfPresent(K key, BiFunction<? super K,​? super V,​? extends V> remappingFunction) If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.

如果传给该方法的key参数在Map中对应的 value不为null,该方法将使用remappingFunction根据原keyvalue计算一个新的结果

  • 如果计算结果不为null,则使用该结果覆盖原来的value;
  • 如果计算结果为null,则删除原key-value对;

forEach方法

方法 描述
default void forEach(BiConsumer<? super K,​? super V> action) Performs the given action for each entry in this map until all entries have been processed or the action throws an exception.

该方法是Java 8Map新增的一个遍历key-value对的方法通过该方法可以更简洁地遍历Mapkey-value对。

getOrDefault方法

方法 描述
default V getOrDefault(Object key, V defaultValue) Returns the value to which the specified key is mapped, or defaultValue if this map contains no mapping for the key.

获取指定key对应的value。如果该key不存在,则返回defaultValue.

merge方法

方法 描述
default V merge(K key, V value, BiFunction<? super V,​? super V,​? extends V> remappingFunction) If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value.

该方法会先根据key参数获取该Map中对应的value

  • 如果获取的valuenull,则直接用传入的value覆盖原有的value(在这种情况下,可能要添加一组key-value对);
  • 如果获取的value不为null,则使用remappingFunction函数根据原value、新value计算一个新的结果,并用得到的结果去覆盖原有的value.

putIfAbsent方法

方法 描述
default V putIfAbsent(K key, V value) If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.

该方法会自动检测指定key对应的value是否为null.

  • 如果该key对应的valuenull,该方法将会用新value代替原来的null值。

replace方法

方法 描述
Object replace(Object key, Object value) Map中指定key对应的value替换成新value。与传统put()方法不同的是,该方法不可能添加新的key-value对。如果尝试替换的key在原Map中不存在,该方法不会添加key-value对,而是返回null.
boolean replace(K key,V oldValue, V newValue) Map中指定key-value对的原value替换成新value。如果在Map中找到指定的key-value对,则执行替换并返回true,否则返回false.

replaceAll方法

方法 描述
default V replace(K key, V value) Replaces the entry for the specified key only if it is currently mapped to some value.

该方法使用BiFunction对原key-value对执行计算,并将计算结果作为该key-value对的value值。

程序 Map常用默认方法测试

下面程序示范了Map常用默认方法的功能和用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.*;

public class MapTest2 {
public static void main(String[] args) {
Map map = new HashMap();
// 成对放入多个key-value对
map.put("疯狂Java讲义", 109);
map.put("疯狂iOS讲义", 99);
map.put("疯狂Ajax讲义", 79);
// 尝试替换key为"疯狂XML讲义"的value,由于原Map中没有对应的key,
// 因此对Map没有改变,不会添加新的key-value对
map.replace("疯狂XML讲义", 66);
System.out.println(map);
// 使用原value与参数计算出来的结果覆盖原有的value
// Object merge(Object key, Object value, BiFunction remappingFunction)`
map.merge("疯狂iOS讲义", 10, (oldVal, param) -> (Integer) oldVal + (Integer) param);
System.out.println(map); // "疯狂iOS讲义"的value增大了10
// 当key为"Java"对应的value为null(或不存在时),使用计算的结果作为新value
// Object computeIfAbsent(object key, Function mappingFunction)
map.computeIfAbsent("Java", (key) -> ((String) key).length());
System.out.println(map); // map中添加了 Java=4 这组key-value对
// 当key为"Java"对应的value存在时,使用计算的结果作为新value
// Object computeIfPresent(Object key, BiFunction remappingFunction)
map.computeIfPresent("Java", (key, value) -> (Integer) value * (Integer) value);
System.out.println(map); // map中 Java=4 变成 Java=16
}
}

运行效果:

1
2
3
4
{疯狂Ajax讲义=79, 疯狂iOS讲义=99, 疯狂Java讲义=109}
{疯狂Ajax讲义=79, 疯狂iOS讲义=109, 疯狂Java讲义=109}
{Java=4, 疯狂Ajax讲义=79, 疯狂iOS讲义=109, 疯狂Java讲义=109}
{Java=16, 疯狂Ajax讲义=79, 疯狂iOS讲义=109, 疯狂Java讲义=109}

上面程序中注释已经写得很清楚了,而且给出了每个方法的运行结果,读者可以结合这些方法的介绍文档来阅读该程序,从而掌握Map中这些默认方法的功能与用法。

8.6 Java 8增强的Map集合

Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,keyvalue都可以是任何引用类型的数据。

Map的key不可重复

Mapkey不允许重复,即同一个Map对象的任何两个key通过equals()方法比较总是返回false

一个key对应一个value

keyvalue之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value

根据key来获取value

Map中取出数据时,只要给出指定的key,就可以取出对应的value

如果把Map的两组值拆开来看,Map里的数据有如图8.7所示的结构。

这里有一张图片

Map中的所有key可以看成一个Set集合

从图8.7中可以看出,如果把Map里的所有key放在一起来看,它们就组成了一个Set集合(所有的key没有顺序,keykey之间不能重复)。

keySet()方法

实际上Map确实包含了一个keySet()方法,用于返回Map里所有key组成的Set集合。

方法 描述
Set<K> keySet() Returns a Set view of the keys contained in this map.

不仅如此,Mapkey集和Set集合里元素的存储形式也很像,Map子类和Set子类在名字上也惊人地相似,比如Set接口下有HashSetLinkedHashSetSortedSet(接口)、 TreeSetEnumSet等子接口和实现类,而Map接口下则有HashMapLinkedHashMapSortedMap(接口)、 TreeMapEnumMap等子接口和实现类。正如它们的名字所暗示的,Map的这些实现类和子接口中key集的存储形式和对应Set集合中元素的存储形式完全相同。

提示

SetMap之间的关系非常密切。虽然Map中放的元素是key-value对,Set集合中放的元素是单个对象,但如果把key-value对中的value当成key的附庸:key在哪里, value就跟在哪里。这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,而计算Entry存储时则只考虑Entry封装的key

Map接口的内部接口Entry 描述
static interface Map.Entry<K,​V> A map entry (key-value pair).

Java使用Map来实现Set

Java源码来看,Java是先实现了Map,然后通过包装一个所有value都为nullMap就实现了Set集合

Map中的的所有value可以看成一个List集合

如果把Map里的所有value放在一起来看,它们又非常类似于一个List

  • 元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象作为索引。
  • 如果需要从List集合中取出元素,则需要提供该元素的数字索引;
  • 如果需要从Map中取出元素,则需要提供该元素的key索引。

因此,Map有时也被称为字典,或关联数组

Map接口方法

Map接口中定义了如下常用方法。

添加元素的方法

方法 描述
V put(K key, V value) 添加一个key-value对,如果当前Map中已有一个与该key相等的key-value对,则新的key-value对会覆盖原来的key-value对。
void putAll(Map<? extends K,? extends V> m) 将指定Map中的key-value对复制到当前Map中。

查询方法

方法 描述
Object get(Object key) 返回指定key所对应的value;如果此Map中不包含该key,则返回null
int size() 返回该Map里的key-value对的个数。

删除元素的方法

方法 描述
V remove(Object key) 删除指定key所对应的key-value对,返回被删除key所关联的value,如果该key不存在,则返回null
default boolean remove(Object key, Object value) 这是Java8新增的方法,删除指定keyvalue所对应的key-value对。如果从该Map中成功地删除该key-value对,该方法返回true,否则返回false
void clear() 删除该Map对象中的所有key-value对.

判断方法

方法 描述
boolean isEmpty() 查询该Map是否为空(即不包含任何key-value对),如果为空则返回true
boolean containsKey(object key) 查询Map中是否包含指定的key,如果包含则返回true
boolean containsValue(Object value) Returns true if this map maps one or more keys to the specified value.

返回集合

方法 描述
Set keySet() 返回该Map中所有key组成的Set集合。
Set entrySet() 返回Map中包含的key-value对所组成的Set集合,每个集合元素都是 Map.Entry对象(EntryMap的内部类)。
Collection values() 返回该Map里所有 value组成的 Collection

Map的实现类

Map接口提供了大量的实现类,典型实现如HashMapHashtable等、 HashMap的子类LinkedHashMap,还有SortedMap子接口及该接口的实现类TreeMap,以及WeakHashMapIdentityHashMap等。下面将详细介绍Map接口实现类。

Map的内部类Entry

Map中包括一个内部类Entry,该类封装了一个key-value对。 Entry包含如下三个方法。

Mapt.Entry内部接口的方法 描述
K getKey() 返回该Entry里包含的key值。
V getValue() 返回该Entry里包含的value值。
V setValue(V value) 设置该Entry里包含的value值,并返回新设置的value值。

Map集合最典型的用法就是成对地添加删除 key-value对,接下来即可判断该Map中是否包含指定key,是否包含指定value,也可以通过Map提供的keySet()方法获取所有key组成的集合,进而遍历Map中所有的key-value对。

程序 Map接口基本用法

下面程序示范了Map的基本功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.*;

public class MapTest
{
public static void main(String[] args)
{
Map map = new HashMap();
// 成对放入多个key-value对
map.put("疯狂Java讲义" , 109);
map.put("疯狂iOS讲义" , 10);
map.put("疯狂Ajax讲义" , 79);
// 多次放入的key-value对中value可以重复
map.put("轻量级Java EE企业应用实战" , 99);
// 放入重复的key时,新的value会覆盖原有的value
// 如果新的value覆盖了原有的value,该方法返回被覆盖的value
System.out.println(map.put("疯狂iOS讲义" , 99)); // 输出10
System.out.println(map); // 输出的Map集合包含4个key-value对
// 判断是否包含指定key
System.out.println("是否包含值为 疯狂iOS讲义 key:"
+ map.containsKey("疯狂iOS讲义")); // 输出true
// 判断是否包含指定value
System.out.println("是否包含值为 99 value:"
+ map.containsValue(99)); // 输出true
// 获取Map集合的所有key组成的集合,通过遍历key来实现遍历所有key-value对
for (Object key : map.keySet() )
{
// map.get(key)方法获取指定key对应的value
System.out.println(key + "-->" + map.get(key));
}
map.remove("疯狂Ajax讲义"); // 根据key来删除key-value对。
System.out.println(map); // 输出结果不再包含 疯狂Ajax讲义=79 的key-value对
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
{疯狂Ajax讲义=79, 疯狂iOS讲义=99, 轻量级Java EE企业应用实战=99, 疯狂Java讲义=109}
是否包含值为 疯狂iOS讲义 key:true
是否包含值为 99 value:true
疯狂Ajax讲义-->79
疯狂iOS讲义-->99
轻量级Java EE企业应用实战-->99
疯狂Java讲义-->109
{疯狂iOS讲义=99, 轻量级Java EE企业应用实战=99, 疯狂Java讲义=109}

上面程序中先示范了向Map中成对地添加key-value对。添加key-value对时,Map允许多个value重复,但如果添加key-value对时Map中已有重复的key,那么新添加的value会覆盖该key原来对应的value并返回被覆盖的value

程序接下来的代码分别判断了Map集合中是否包含指定的key、是否包含指定的value

使用foreach循环 遍历Map集合

程序中的foreach循环用于遍历Map集合:

  • 先调用Map集合的keySet()获取包含所有key的Set集合,
  • 然后使用foreach循环来遍历这个Set集合中的key,
  • 最后根据key来获取对应的value

所有的Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式的字符串:{key1=value,key2=value2…}.

8.5.4 各种线性表的性能分析

线性表

Java提供的List就是一个线性表接口,而ArrayListLinkedList又是线性表的两种典型实现:基于数组的线性表基于链表的线性表

队列

Queue代表了队列

双端队列

Deque代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。

初学者可以无须理会ArrayListLinkedList之间的性能差异,只需要知道LinkedList集合不仅提供了List的功能,还提供了双端队列* 、*栈 的功能**就行。但对于一个成熟的Java程序员,在一些性能非常敏感的地方,可能需要慎重选择哪个List实现。

数组 随机访问性能好

一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;

链表 插入 删除操作性能好

内部以链表作为底层实现的集合在执行插入删除操作时有较好的性能。

优先考虑使用ArrayList

但总体来说, ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑使用ArrayList.

List集合使用建议

应该使用随机访问方法get()来遍历ArrayList

对于ArrayListVector集合,应该使用随机访问方法(也就是get())来遍历集合元素,这样性能更好;

应该使用迭代器遍历LinkedList

对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素.

频发插入 删除时使用LinkedList

如果需要经常执行插入删除操作来改变包含大量数据的List集合的大小,可考虑使用LinkedList集合。
使用ArrayListVector集合可能需要经常重新分配内部数组的大小,效果可能较差。

Collections功能将List集合变成线程安全

如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。

8.5.3 LinkedList实现类

List接口的功能 按索引访问

LinkedList类是List接口的实现类,这意味着它是一个List集合,所以可以根据索引来随机访问集合中的元素。

LinkedList 作为栈 作为队列使用

除此之外, LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此,LinkedList既可以被当成”“来使用,也可以当成队列使用

程序 LinkedList示例

下面程序简单示范了LinkedList集合的用法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.*;

public class LinkedListTest
{
public static void main(String[] args)
{
LinkedList books = new LinkedList();
// 将字符串元素加入队列的尾部
books.offer("疯狂Java讲义");
// 将一个字符串元素加入栈的顶部
books.push("轻量级Java EE企业应用实战");
// 将字符串元素添加到队列的头部(相当于栈的顶部)
books.offerFirst("疯狂Android讲义");
// 以List的方式(按索引访问的方式)来遍历集合元素
for (int i = 0; i < books.size() ; i++ )
{
System.out.println("遍历中:" + books.get(i));
}
// 访问、并不删除栈顶的元素
System.out.println(books.peekFirst());
// 访问、并不删除队列的最后一个元素
System.out.println(books.peekLast());
// 将栈顶的元素弹出“栈”
System.out.println(books.pop());
// 下面输出将看到队列中第一个元素被删除
System.out.println(books);
// 访问、并删除队列的最后一个元素
System.out.println(books.pollLast());
// 下面输出:[轻量级Java EE企业应用实战]
System.out.println(books);
}
}

上面程序中粗体字代码分别示范了LinkedList作为List集合、双端队列的用法。由此可见,LinkedList是一个功能非常强大的集合类。

ArrayList ArrayDeque和LinkedList的对比

LinkedListArrayListArrayDeque的实现机制完全不同,

  • ArrayListArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;
  • LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色(只需改变指针所指的地址即可)。

需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能(而且实现机制也不好),所以各方面性能都比较差

基于数组的集合随机访问性能比Iterator迭代性能好

对于所有的内部基于数组的集合实现,例如ArrayListArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。

8.5.2 Deque接口与ArrayDeque实现类

Deque接口是Queue接口的子接口,它代表一个双端队列Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

Deque接口方法

插入队列头部

方法 描述
void addFirst(E e) 将指定元素插入该双端队列的开头。
boolean offerFirst(E e) 将指定元素插入该双端队列的开头。

插入队列尾部

方法 描述
void addLast(E e) 将指定元素插入该双端队列的末尾。
boolean offerLast(E e) 将指定元素插入该双端队列的末尾。

查看队头

方法 描述
E getFirst() 获取但不删除该双端队列的第一个元素。
E peekFirst() 获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null

返回迭代器

方法 描述
Iterator<E> descendingIterator() 返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。

查看队尾

方法 描述
E getLast() 获取但不删除该双端队列的最后一个元素。
E peekLast() 获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null

获取并删除队头

方法 描述
E removeFirst() 获取并删除该双端队列的第一个元素。
E pollFirst() 获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null

获取并删除队尾

方法 描述
E removeLast() 获取并删除该双端队列的最后一个元素。
E pollLast() 获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null

删除第一次出现的元素

方法 描述
boolean removeFirstOccurrence(Object o) 删除该双端队列中的第一次出现的元素o。
boolean removeLastOccurrence(Object o) 删除该双端队列的最后一次出现的元素o。

栈方法

方法 描述
E pop() 这是个栈方法,pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()
void push(E e) 这是个栈方法,将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst(Object e)方法.

Deque的方法与Queue的方法对照

从上面方法中可以看出, Deque不仅可以当成双端队列使用,而且可以被当成来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。
Deque的方法与Queue的方法对照表如下所示

Queue的方法 Deque的方法 描述
add() addLast() 将指定元素加入此队列的尾部
offer() offerLast() 将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
remove() removeFirst() 获取队列头部的元素,并删除该元素。
poll() pollFirst() 获取队列头部的元素,并删除该元素。如果此队列为空,则返回null
element() getFirst() 获取队列头部的元素,但是不删除该元素。
peek() peekFirst() 获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null

Deque的方法与Stack的方法对照

Deque的方法与Stack的方法对照表如下所示

Stack的方法 Deque的方法 描述
push() addFirst()offerFirst() 进栈
pop() removeFirstpollFirst() 出站
peek() getFirst()peekFirst() 查看栈顶

ArrayDeque实现类

Deque接口提供了一个典型的实现类: ArrayDeque,从该名称就可以看出,ArrayDeque是一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object数组的长度;

方法 描述
ArrayDeque(int numElements) Constructs an empty array deque with an initial capacity sufficient to hold the specified number of elements.

如果不指定numElements参数, 则Deque默认的底层数组的长度为16

提示
ArrayListArrayDeque两个集合类的实现机制基本相似,它们的底层都采用一个动态的、可重分配的Object数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个Object数组来存储集合元素

程序 ArrayDeque当成栈使用

下面程序示范了把ArrayDeque当成”栈”来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class ArrayDequeStack {
public static void main(String[] args) {
ArrayDeque stack = new ArrayDeque();
// 依次将三个元素push入"栈"
stack.push("疯狂Java讲义");
stack.push("轻量级Java EE企业应用实战");
stack.push("疯狂Android讲义");
// 输出:[疯狂Android讲义, 轻量级Java EE企业应用实战, 疯狂Java讲义]
System.out.println(stack);
// 访问第一个元素,但并不将其pop出"栈",输出:疯狂Android讲义
System.out.println(stack.peek());
// 依然输出:[疯狂Android讲义, 疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(stack);
// pop出第一个元素,输出:疯狂Android讲义
System.out.println(stack.pop());
// 输出:[轻量级Java EE企业应用实战, 疯狂Java讲义]
System.out.println(stack);
// 继续
System.out.println(stack.pop());
System.out.println(stack);
}
}

运行结果:

1
2
3
4
5
6
7
[疯狂Android讲义, 轻量级Java EE企业应用实战, 疯狂Java讲义]
疯狂Android讲义
[疯狂Android讲义, 轻量级Java EE企业应用实战, 疯狂Java讲义]
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Java讲义]
轻量级Java EE企业应用实战
[疯狂Java讲义]

需要时用ArrayDeque而不是Stack

上面程序的运行结果显示了ArrayDeque作为栈的行为,因此当程序中需要使用”栈”这种数据结构时,推荐使用ArrayDeque,尽量避免使用Stack,因为Stack是古老的集合,性能较差。

程序 ArrayDeque当成队列使用

当然ArrayDeque也可以当成队列使用,此处ArrayDeque将按”先进先出“的方式操作集合元素。
例如如下程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class ArrayDequeQueue {
public static void main(String[] args) {
ArrayDeque queue = new ArrayDeque();
// 依次将三个元素加入队列
queue.offer("疯狂Java讲义");
queue.offer("轻量级Java EE企业应用实战");
queue.offer("疯狂Android讲义");
// 输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// 访问队列头部的元素,但并不将其poll出队列"栈",输出:疯狂Java讲义
System.out.println(queue.peek());
// 依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// poll出第一个元素,输出:疯狂Java讲义
System.out.println(queue.poll());
// 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// 继续
System.out.println(queue.poll());
System.out.println(queue);
}
}

运行结果

1
2
3
4
5
6
7
[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
疯狂Java讲义
[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
疯狂Java讲义
[轻量级Java EE企业应用实战, 疯狂Android讲义]
轻量级Java EE企业应用实战
[疯狂Android讲义]

上面程序的运行结果显示了ArrayDeque作为队列的行为通过上面两个程序可以看出, ArrayDeque不仅可以作为栈使用,也可以作为队列使用

8.5.1 PriorityQueue实现类

PriorityQueue 会自动排序

PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为**PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序**。

因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。

程序 PriorityQueue用法

下面程序示范了PriorityQueue队列的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class PriorityQueueTest
{
public static void main(String[] args)
{
PriorityQueue pq = new PriorityQueue();
// 下面代码依次向pq中加入四个元素
pq.offer(6);
pq.offer(-3);
pq.offer(20);
pq.offer(18);
// 输出pq队列,并不是按元素的加入顺序排列
System.out.println(pq); // 输出[-3, 6, 20, 18]
// 访问队列第一个元素,其实就是队列中最小的元素:-3
System.out.println(pq.poll());
System.out.println(pq.poll());
System.out.println(pq.poll());
}
}

运行效果:

1
2
3
4
[-3, 6, 20, 18]
-3
6
18

PriorityQueue的toString()方法没有排序效果

运行上面程序直接输出PriorityQueue集合时,可能看到该队列里的元素并没有很好地按大小进行排序,但这只是受到PriorityQueuetoString方法的返回值的影响。

poll()方法会从小到大将元素移出队列

实际上,程序多次调用PriorityQueue集合对象的poll()方法时,可看到**元素按从小到大的顺序”移出队列”**。

PriorityQueue中不能加入null

PriorityQueue不允许插入null元素。

自然排序 定制排序

PriorityQueue需要对队列元素进行排序, PriorityQueue的元素有两种排序方式:

  1. 自然排序:采用自然排序的PriorityQueue集合中的元素必须实现了Comparable接口,而且PriorityQueue中应该存放的是同一个类的多个实例,否则可能导致ClassCastException异常。
  2. 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。

PriorityQueue队列对元素的要求与TreeSet对元素的要求基本一致,因此关于使用自然排序和定制排序的详细介绍请参考8.3.3节。

8.5 Queue集合

队列 Queue

Queue用于模拟队列这种数据结构,队列通常是指先进先出(FIFO)的容器。

  • 队列的头部是保存在队列中存放时间最长的元素,
  • 队列的尾部是保存在队列中存放时间最短的元素。
  • 新元素插入(offer)到队列的尾部,
  • 访问元素(poll)操作会返回队列头部的元素。
  • 通常,队列不允许随机访问队列中的元素

Queue接口方法

Queue接口中定义了如下几个方法:

在队列尾部添加元素的方法

方法 描述
boolean add(E e) 将指定元素加入此队列的尾部
boolean offer(E e) 将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。

查看队列头部

方法 描述
E element() 获取队列头部的元素,但是不删除该元素。
E peek() 获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null

摘下队列头部

方法 描述
E poll() 获取队列头部的元素,并删除该元素。如果此队列为空,则返回null
E remove() 获取队列头部的元素,并删除该元素。

Queue接口的常用实现类

Queue接口有一个PriorityQueue

双端队列 Deque接口

除此之外, Queue还有一个Deque接口, Deque代表一个”双端队列”,
双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列使用,也可当成栈使用

Deque接口的常用实现类

JavaDeque提供了ArrayDequeLinkedList两个实现类.

8.4.3 固定长度的List

将数组转成List集合

前面讲数组时介绍了一个操作数组的工具类: Arrays,Arrays工具类里提供了asList方法:

ArraysasList方法 描述
static <T> List<T> asList(T... a) Returns a fixed-size list backed by the specified array.

该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例

Arrays.ArrayList内部类

Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素
如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.*;

public class FixedSizeList {
public static void main(String[] args) {
List fixedList = Arrays.asList("疯狂Java讲义", "轻量级Java EE企业应用实战");
// 获取fixedList的实现类,将输出Arrays$ArrayList
System.out.println(fixedList.getClass());
// 使用方法引用遍历集合元素
fixedList.forEach(System.out::println);
// 试图增加、删除元素都会引发UnsupportedOperationException异常
fixedList.add("疯狂Android讲义"); // 代码1
fixedList.remove("疯狂Java讲义"); // 代码2
}
}

运行效果如下:

1
2
3
4
5
6
7
class java.util.Arrays$ArrayList
疯狂Java讲义
轻量级Java EE企业应用实战
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at FixedSizeList.main(FixedSizeList.java:11)

不要增加或删除Arrays.ArrayList内部类

上面程序中代码1,代码2这两行代码对于普通的List集合完全正常,但如果试图通过这两个方法来增加删除Arrays.ArrayList集合里的元素,将会引发异常。
所以上面程序在编译时完全正常,但会在运行代码1处引发UnsupportedOperationException异常。

8.4.2 ArrayList和Vector实现类

ArrayList和Vector

ArrayListVector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。
ArrayListVector类都是基于数组实现的List,所以ArrayListVector类封装了一个动态的、允许再分配的Object数组。
ArrayListVector对象使用initialCapacity参数来设置该数组的长度,当向ArrayListVector中添加元素超出了该数组的长度时,它们的initialCapacity会自动增加。

使用ensureCapacity方法增加ArrayList容器

对于通常的编程场景,程序员无须关心ArrayListVectorinitialCapacity。但如果向ArrayListVector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。

如果开始就知道ArrayListVector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。

方法 描述
ArrayList(int initialCapacity) Constructs an empty list with the specified initial capacity.
方法 描述
Vector(int initialCapacity) Constructs an empty vector with the specified initial capacity and with its capacity increment equal to zero.

ArrayList或Vector默认容量为10

如果创建空的ArrayListVector集合时不指定initialCapacity参数,则Object数组的长度默认为10

除此之外, ArrayListVector还提供了如下两个方法来重新分配Object数组。

方法 描述
void ensureCapacity(int minCapacity) ArrayListVector集合的Object数组长度增加大于或等于minCapacity值。
void trimToSize() 调整ArrayListVector集合的Object数组长度为当前元素的个数。调用该方法可减少ArrayListVector集合对象占用的存储空间.

尽量不要用Vector

ArrayListVector在用法上几乎完全相同,但由于Vector是一个古老的集合(从JDK 1.0就有了),那时候Java还没有提供系统的集合框架,所以Vector里提供了一些方法名很长的方法,例如addElement(Object obj),实际上这个方法与add(Object obj)没有任何区别。从JDK1.2以后,Java提供了系统的集合框架,就将Vector改为List接口实现之一了,从而导致Vector里有一些功能重复的方法。
Vector的系列方法中方法名更短的方法属于后来新增的方法,方法名更长的方法则是Vector原有的方法。Java改写了Vector原有的方法,将其方法名缩短是为了简化编程。而ArrayList开始就作为List接口的主要实现类,因此没有那些方法名很长的方法。实际上, Vector具有很多缺点,通常**尽量少用Vector**这个实现类。

ArrayList线程不安全 Vector线程安全

除此之外, ArrayListVector的显著区别是: ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;
但**Vector集合则是线程安全的**,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也同样不推荐使用Vector这个实现类。后面会介绍一个**Collections工具类,它可以将一个ArrayList变成线程安全的**。

Stack类

Vector还提供了一个Stack子类,它用于模拟”栈”这种数据结构,”栈”通常是指”后进先出“(LIFO)的容器。最后”push“进栈的元素,将最先被”pop“出栈。与Java中的其他集合一样,进栈出栈的都是Object,因此从栈中取出元素后必须进行类型转换,除非你只是使用Object具有的操作。

Stack类方法

方法 描述
E peek() 返回”栈”的第一个元素,但并不将该元素”pop“出栈。
E pop() 返回“栈”的第一个元素,并将该元素“pop”出栈。
E push(E item) 将一个元素”push“进栈,最后一个进”栈”的元素总是位于”栈”顶。
boolean empty() Tests if this stack is empty.
int search(Object o) Returns the 1-based position where an object is on this stack.

尽量少用Stack

需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,Stack同样是线程安全、性能较差的,因此应该尽量少用Stack类。如果程序需要使用”栈”这种数据结构,则可以考虑使用后面将要介绍的ArrayDeque

ArrayDeque

ArrayDeque也是List的实现类, ArrayDeque既实现了List接口,也实现了Deque接口,由于实现了Deque接口,因此可以作为栈来使用;而且ArrayDeque底层也是基于数组的实现,因此性能也很好。本书将在8.5节详细介绍ArrayDeque

8.4.1 Java 8改进的List接口和ListIterator接口

List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里增加了一些根据索引来操作集合元素的方法。

List集合中增加的索引相关的方法

根据索引 增 删 改 查元素

List新增的根据索引增删改查方法 描述
void add(int index, E element) 将元素element插入到List集合的index
boolean addAll(int index, Collection<? extends E> c) 将集合c所包含的所有元素都插入到List集合的index
E remove(int index) 删除并返回index索引处的元素。
E set(int index, E element) index索引处的元素替换成element对象,返回被替换的旧元素。
E get(int index) 返回集合index索引处的元素。

set方法不会改变集合长度

当调用Listset(int index, Object element)方法来更新List集合指定索引处的元素时,指定的索引必须是List集合的有效索引。
例如,集合长度是4,就不能指定替换索引为4处的元素。也就是说,set(int index, Object element)方法不会改变List集合的长度.

查询元素的索引

方法 描述
int indexOf(Object o) 返回对象oList集合中第一次出现的位置索引。
int lastIndexOf(Object o) 返回对象oList集合中最后一次出现的位置索引。

截取子List

方法 描述
List<E> subList(int fromIndex, int toIndex) 返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合。subXxxx方法一般遵循前闭后开原则。

所有的List实现类都可以调用这些方法来操作集合元素。与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法

Java8增加的默认方法

除此之外,Java 8还为List接口添加了如下两个默认方法

方法 描述
default void replaceAll(UnaryOperator<E> operator) 根据operator指定的计算规则重新设置List集合的所有元素
default void sort(Comparator<? super E> c) 根据Comparator参数对List集合的元素排序

程序 List集合常规用法

下面程序示范了List集合的常规用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;

public class ListTest
{
public static void main(String[] args)
{
List books = new ArrayList();
// 向books集合中添加三个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
// 将新字符串对象插入在第二个位置
books.add(1 , new String("疯狂Ajax讲义"));
for (int i = 0 ; i < books.size() ; i++ )
{
System.out.println(books.get(i));
}
// 删除第三个元素
books.remove(2);
System.out.println(books);
// 判断指定元素在List集合中位置:输出1,表明位于第二位
System.out.println(books.indexOf(new String("疯狂Ajax讲义"))); //①
//将第二个元素替换成新的字符串对象
books.set(1, new String("疯狂Java讲义"));
System.out.println(books);
//将books集合的第二个元素(包括)
//到第三个元素(不包括)截取成子集合
System.out.println(books.subList(1 , 2));
}
}

可以使用普通的for循环遍历List集合

List集合可以根据位置索引来访问集合中的元素,因此List增加了一种新的遍历集合元素的方法:使用普通的for循环来遍历List集合元素。
运行上面程序,将看到如下运行结果:

1
2
3
4
5
6
7
8
9
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
轻量级Java EE企业应用实战
疯狂Ajax讲义
疯狂Java讲义
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Ajax讲义, 疯狂Android讲义]
1
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义]

从上面运行结果清楚地看出List集合的用法。注意①行代码处,程序试图返回新字符串对象在List集合中的位置,实际上List集合中并未包含该字符串对象。因为List集合添加字符串对象时,添加的是通过new关键字创建的新字符串对象,①行代码处也是通过new关键字创建的新字符串对象,两个字符串显然不是同一个对象,但Listindexof方法依然可以返回1。List判断两个对象相等的标准是什么呢?

List如何判断两个对象相等

两个对象相等只要通过equals方法比较返回true,则List就认为这两个元素相等

程序 List通过equals方法来判断元素是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.*;

class A
{
public boolean equals(Object obj)
{
return true;
}
}
public class ListTest2
{
public static void main(String[] args)
{
List books = new ArrayList();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
// 删除集合中A对象,将导致第一个元素被删除
books.remove(new A()); // ①
System.out.println(books);
// 删除集合中A对象,再次删除集合中第一个元素
books.remove(new A()); // ②
System.out.println(books);
}
}

编译、运行上面程序,看到如下运行结果:

1
2
3
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义, 疯狂Android讲义]
[疯狂Android讲义]

从上面运行结果可以看出,执行①行代码时,程序试图删除一个A对象,List将会调用该A对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素。
由于A类重写了equals方法,该方法总是返回true。所以每次从List集合中删除A对象时,总是删除List集合中的第一个元素。

java 8中List集合增加的方法

Java 8List集合增加了sort()replaceAll()两个常用的默认方法,其中

  • sort()方法需要一个Comparator对象来控制元素排序,程序可使用Lambda表达式来作为参数;
  • replaceAll()方法则需要个UnaryOperator来替换所有集合元素, UnaryOperator也是一个函数式接口,因此程序也可使用Lambda表达式作为参数。

程序 测试List在Java 8中新增的默认方法

如下程序示范了List集合的两个默认方法的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

public class ListTest3
{
public static void main(String[] args)
{
List books = new ArrayList();
// 向books集合中添加4个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
books.add(new String("疯狂IOS讲义"));
// 使用目标类型为Comparator的Lambda表达式对List集合排序
books.sort((o1, o2)->((String)o1).length() - ((String)o2).length());//①
System.out.println(books);
// 使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
// 该Lambda表达式控制使用每个字符串的长度作为新的集合元素
books.replaceAll(ele->((String)ele).length());//②
System.out.println(books); // 输出[7, 8, 11, 16]
}
}
1
2
[疯狂IOS讲义, 疯狂Java讲义, 疯狂Android讲义, 轻量级Java EE企业应用实战]
[7, 8, 11, 16]

上面程序中编号为①的代码控制对List集合进行排序,传给sort()方法的Lambda表达式指定的排序规则是:字符串长度越长,字符串越大,因此执行完第一行粗体字代码之后,List集合中的字符串按由短到长的顺序排列.
程序中编号为②的代码传给replaceAll()方法的Lambda表达式指定了替换集合元素的规则:直接用集合元素(字符串)的长度作为新的集合元素。执行该方法后,**集合元素被替换为[7,8,11,16]**。

List集合独有的listIterator方法

Set只提供了一个iterator()方法不同,List额外提供了一个listIterator()方法:

方法 描述
ListIterator<E> listIterator() Returns a list iterator over the elements in this list (in proper sequence).

该方法返回一个ListIterator对象, ListIterator接口继承了Iterator接口,提供了专门操作List的方法

ListIterator接口增加的方法

ListIterator接口在Iterator接口基础上增加了如下方法。

方法 描述
boolean hasPrevious() 返回该迭代器关联的集合是否还有上一个元素。
E previous() 返回该迭代器的上一个元素
void add(E e) Inserts the specified element into the list (optional operation).

ListIterator和Iterator的对比

  • ListIterator增加了向前迭代的功能,而**Iterator只能向后迭代**
  • ListIterator可通过add()方法向List集合中添加元素,而Iterator只能删除元素.

程序 List独有的迭代器ListIterator的用法

下面程序示范了ListIterator的用法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.*;

public class ListIteratorTest {
public static void main(String[] args) {
String[] books = { "小明", "小王", "小张" };
List bookList = new ArrayList();
for (int i = 0; i < books.length; i++) {
bookList.add(books[i]);
}

ListIterator lit = bookList.listIterator();
while (lit.hasNext()) {
System.out.println(lit.next());
lit.add("小芳");
}

System.out.println("=======下面开始反向迭代=======");
while (lit.hasPrevious()) {
System.out.println(lit.previous());
}
}
}

从上面程序中可以看出,使用ListIterator迭代List集合时,开始也需要采用正向迭代,即先使用next()方法进行迭代,在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素。
先正向迭代,然后再反向迭代
运行上面程序,看到如下结果:

1
2
3
4
5
6
7
8
9
10
小明
小王
小张
=======下面开始反向迭代=======
_小芳
小张
_小芳
小王
_小芳
小明