13 Java 集合

集合框架体系概述

  1. 为什么出现集合类?
    方便多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方法.

  2. 数组和集合类同时容器, 有何不可?

  • 数组虽然也可存储对象,但长度固定; 而集合长度可变
  • 集合只用于存储对象, 集合长度是可变的, 集合可以存储不同类型的对象.

Java 集合定义了两种基本的数据结构,一种是 Collection,表示一组对象的集合;另一种是Map,表示对象间的一系列映射或关联关系。Java 集合的基本架构如下图。

集合类及其继承关系

在这种架构中,Set 是一种 Collection,不过其中没有重复的对象; List 也是一种Collection ,其中的元素按顺序排列(不过可能有重复)。
SortedSetSortedMap 是特殊的集和映射,其中的元素按顺序排列。
CollectionSetListMapSortedSetSortedMap 都是接口,不过 java.util 包定义了多个具体实现,例如基于数组和链表的列表,基于哈希表或二叉树的映射和集。除此之外,还有两个重要的接口, Iterator 和 Iterable ,用于遍历集合中的对象,稍后会介绍。

Collection共性方法

注意: 集合存储的都是对象的引用(地址)

boolean add(E e)添加指定的元素(可选操作)     
boolean addAll(Collection<? extends E> c) 将指定 collection 中的所有元素都添加到此 collection 中(可选操作)。 
clear(): 清空集合中所有元素     
boolean contains(Object o) 是否包含指定元素     
boolean containsAll(Collection<?> c) 只判断参数中的集合是否都包含在A集合内,最终A集合没有任何变化.     
boolean isEmpty() 判断是否为空     
boolean remove(Object o)   移除单个实例     
boolean removeAll(Collection<?> c)  取差集    
boolean retainAll(Collection<?> c)  取交集     
int size():返回collection中的元素     
Object[] toArray() 这个可以理解     
<T> T[]  toArray(T[] a)  //应这么写String[] y =  c.toArray(new String[collection.size()])较好
// 创建几个集合,供后面的代码使用
Collection<String> c = new HashSet<>(); // 一个空集
Collection<String> d = Arrays.asList("one", "two");
Collection<String> e = Collections.singleton("three");

// 向集合中添加一些元素
// 如果集合的内容变化了,这些方法返回true
// 这种表现对不允许重复的Set类型很有用
c.add("zero");           // 添加单个元素
c.addAll(d);             // 添加d中的所有元素

// 复制集合:多数实现都有副本构造方法
Collection<String> copy = new ArrayList<String>(c);

// 把元素从集合中移除。
// 除了clear()方法之外,如果集合的内容变化了,都返回true
c.remove("zero");        // 移除单个元素
c.removeAll(e);          // 移除一组元素
c.retainAll(d);          // 移除不在集合d中的所有元素
c.clear();               // 移除所有元素

// 获取集合的大小
boolean b = c.isEmpty(); // c是空的,所以返回true
int s = c.size();        // 现在c的大小是0

// 使用前面创建的副本复原集合
c.addAll(copy);

// 测试元素是否在集合中。测试基于equals()方法,而不是==运算符
b = c.contains("zero");  // true
b = c.containsAll(d);    // true

// 多数Collection实现都有toString()方法
System.out.println(c);

// 使用集合中的元素创建一个数组。
// 如果迭代器能保证特定的顺序,数组就有相同的顺序
// 得到的数组是个副本,而不是内部数据结构的引用
Object[] elements = c.toArray();

// 如果想把集合中的元素存入String[]类型的数组,必须在参数中指定这个类型
String[] strings = c.toArray(new String[c.size()]);

// 或者传入一个类型为String[]的空数组,指定所需的类型
// toArray()方法会为这个数组分配空间
strings = c.toArray(new String[0]);

记住,上述各个方法都能用于 Set、List 或 Queue。这几个子接口可能会对集合中的元素做些限制或有顺序上的约束,但都提供了相同的基本方法。

修改集合的方法,例如 add()、remove()、clear() 和 retainAll(),是可选的 API。不过,这个规则在很久以前就定下了,那时认为如果不提供这些方法,明智的做法是抛出 UnsupportedOperationException 异常。因此,某些实现(尤其是只读方法)可能会抛出未检异常。

  • Collection (集合)和 Map(映射) 及其子接口都没扩展 Cloneable 或 Serializable 接口。不过,在 Java 集合框架中,实现集合和映射的所有类都实现了这两个接口。
  • 有些集合对其可以包含的元素做了限制。例如,有的集合禁止使用 null 作为元素。EnumSet 要求其中的元素只能是特定的枚举类型。
  • 如果尝试把禁止使用的元素添加到集合中,会抛出未检异常,例如 NullPointerExceptionClassCastException。检查集合中是否包含禁止使用的元素,可能也会抛出这种异常,或者仅仅返回 false。

List接口

List 是一组有序的对象集合。列表中的每个元素都有特定的位置,而且 List 接口定义了一些方法,用于查询或设定特定位置(或叫索引)的元素。从这个角度来看,List 对象和数组类似,不过列表的大小能按需变化,以适应其中元素的数量。和Set不同,列表允许出现重复的元素。

除了基于索引的 get() 和 set() 方法之外,List 接口还定义了一些方法,用于把元素添加到特定的索引,把元素从特定的索引移除,或者返回指定值在列表中首次出现或最后出现的索引。从 Collection 接口继承的 add() 和 remove() 方法,前者把元素添加到列表末尾,后者把指定值从列表中首次出现的位置移除。继承的 addAll() 方法把指定集合中的所有元素添加到列表的末尾,或者插入指定的索引。retainAll() 和 removeAll() 方法的表现与其他 Collection 对象一样,如果需要,会保留或删除多个相同的值。

List 接口没有定义操作索引范围的方法,但是定义了一个 subList() 方法。这个方法返回一个 List 对象,表示原列表指定范围内的元素。子列表会回馈父列表,只要修改了子列表,父列表立即就能察觉到变化。下述代码演示了 subList() 方法和其他操作 List 对象的基本方法:

// 创建两个列表,供后面的代码使用
List<String> l = new ArrayList<String>(Arrays.asList(args));
List<String> words = Arrays.asList("hello", "world");

// 通过索引查询和设定元素
String first = l.get(0);        // 列表的第一个元素
String last = l.get(l.size -1); // 列表的最后一个元素
l.set(0, last);                 // 把最后一个元素变成第一个元素

// 添加和插入元素
// add()方法既可以把元素添加到列表末尾,也可以把元素插入指定索引
l.add(first);       // 把第一个词添加到列表末尾
l.add(0, first);    // 再把第一个词添加到列表的开头
l.addAll(words);    // 把一个集合添加到列表末尾
l.addAll(1, words); // 在第一个词之后插入一个集合

// 子列表:回馈原列表
List<String> sub = l.subList(1,3); // 第二个和第三个元素
sub.set(0, "hi");                  // 修改l的第二个元素
// 子列表可以把操作限制在原列表索引的子范围内
String s = Collections.min(l.subList(0,4));
Collections.sort(l.subList(0,4));
// 子列表的独立副本不影响父列表
List<String> subcopy = new ArrayList<String>(l.subList(1,3));

// 搜索列表
int p = l.indexOf(last); // 最后一个词在哪个位置?
p = l.lastIndexOf(last); // 反向搜索

// 打印last在l中出现的所有索引。注意,使用了子列表
int n = l.size();
p = 0;
do {
    // 创建一个列表,只包含尚未搜索的元素
    List<String> list = l.subList(p, n);
    int q = list.indexOf(last);
    if (q == -1) break;
    System.out.printf("Found '%s' at index %d%n", last, p+q);
    p += q+1;
} while(p < n);

// 从列表中删除元素
l.remove(last);         // 把指定元素从首次出现的位置上删除
l.remove(0);            // 删除指定索引对应的元素
l.subList(0,2).clear(); // 使用subList()方法,删除一个范围内的元素
l.retainAll(words);     // 删除所有不在words中的元素
l.removeAll(words);     // 删除所有在words中的元素
l.clear();              // 删除所有元素

重点讲讲用于查找的Iterator迭代器接口
Iterator it = al.iterator();
实际上是集合类在List和Set都包含的iterator方法,返回Iterator对象,具体实现方式是内部类.可以认为是继承了AbstractList,实现了Iterable接口.把取出方式定义成内部类,每个容器的数据结构不同,取出的动作细节也不一样.但是都用共性的判断和取出,可以将共性方法抽取.对外提供了Iterator方法.

集合中的迭代器
     //一般写法
     while(it.hasNext()){
          System.out.println(it.next());
     }
 
     //老外为了节省空间,写成这样
     for(Iterator it = al.iterator(); it.hasNext(); ){
          System.out.println(it.next());
     }
List共性方法

(List也被成为序列, 它的对象的元素有序可重复,正因为有序,所以操作角标的方法都是该体系特有的方法)

  • 增 void add(int index, E element) 在列表的指定位置插入指定元素(可选操作)。
  • 删 E remove(int index) 移除列表中指定位置的元素(可选操作)。
  • 改 E set(int index, E element) 用指定元素替换列表中指定位置的元素(可选操作)。
  • 查 ListIterator<E> listIterator() 返回此列表元素的列表迭代器(按适当顺序)。
  • 获取 E get(int index) 返回列表中指定位置的元素。
//由于get(index)方法, list因此多了一种取出所有元素的方法. 但还是常用迭代器
for(int i=0;i<al.size();i++){                  
    输出al.get(i);            
} 

获取<E> subList(int fromIndex, int toIndex) 返回列表中指定的 fromIndex(包括)和 toIndex(不包括)之间的部分视图。

ListIterator

List有自己更强功能的的ListIterator是Iterator的子接口,是带下标的.

集合引用和迭代器引用在同时操作元素,通过集合获取到对应的迭代器后,在迭代中,进行集合引用的元素添加,迭代器并不知道,所以会出现ConcurrentModificationException异常情况。ListIterator列表迭代器接口具备了对元素的增、删、改、查的动作。

原 查 next() 但是 增加了previous()
原 删 void remove()
增加了特有了

  • 增void add(E e)
  • 改 void set(E e)
  • 和独有的int nextIndex(), int previousIndex() 和 int nextIndex()
List集合的三个常见子类对象(List有序可重复,因为体系有索引)
  • ArrayList: 底层使用数组结构, 查询块,增删稍慢. 线程不同步, JDK1.2以上
  • LinkedList: 底层是链表结构, 增删块,查询稍慢, 线程不同步, JDK1.2以上
  • Vector: 底层使用数组结构, 查询块,增删慢. 线程同步.被ArrayList替代了. 枚举就是Vector特有的取出方式.

ArrayList详解:拥有角标的方法是其特有方法
可变长度数组的原理 :当元素超出数组长度,会产生一个新数组,将原数组的数据复制到新数组中,再将新的元素添加到新数组中。

  • ArrayList:是按照原数组的 50%延长构造一个初始容量为10的空列表。
  • Vector:是按照原数组的 100%延长

LinkedList详解:
特有的add,get,remove方法

//在1.6后新方法        
boolean offerFirst(E e)                
在此列表的开头插入指定的元素。      
boolean offerLast(E e)                
在此列表末尾插入指定的元素。 
E peek()           
获取但不移除此列表的头(第一个元素)。
E peekFirst()                
获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。           
E peekLast()           
获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。            
E pollFirst()                
获取并移除此列表的第一个元素;如果此列表为空,则返回 null。           
E pollLast()                
获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。 

Vector(过时)详解
枚举是Vector特有的取出方式hasMoreElements()和nextElement()方法,发现枚举和迭代器很像.其实枚举和迭代一样的.

在List下的ArrayList和LinkedList的contains和remove方法都是使用了Object的equals方法. 可以自己重写equals方法判断集合内两对象是否"一致"

随机访问列表中的元素

我们一般期望实现 List 接口的类能高效迭代,而且所用时间和列表的大小成正比。然而,不是所有列表都能高效地随机访问任意索引上的元素。按顺序访问的列表,例如 LinkedList 类,提供了高效的插入和删除操作,但降低了随机访问性能。提供高效随机访问的类都实现了标记接口 RandomAccess,因此,如果需要确定是否能高效处理列表,可以使用 instanceof 运算符测试是否实现了这个接口:

// 随便创建一个列表,供后面的代码处理
List<?> l = ...;

// 测试能否高效随机访问
// 如果不能,先使用副本构造方法创建一个支持高效随机访问的副本,然后再处理
if (!(l instanceof RandomAccess)) l = new ArrayList<?>(l);

在 List 对象上调用 iterator() 方法会得到一个 Iterator 对象,这个对象按照元素在列表中的顺序迭代各元素。List 实现了 Iterable 接口,因此列表可以像其他集合一样使用遍历循环迭代。

下表总结了 Java 平台中五种通用的 List 实现。Vector 和 Stack 类已经过时,别再用了。CopyOnWriteArrayList 类在 java.util.concurrent 包中,只适合在多线程环境中使用。

实现List接口的类

Set接口

  • Set集合的方法和Collection一致,不用多讲, 但对这些方法做了限制, 是无重复对象组成的集合

下表列出了实现 Set 接口的类,而且总结了各个类的内部表示方式、排序特性、对成员的限制,以及 add()、remove()、contains 等基本操作和迭代的性能。这些类的详细信息,请参见各自的文档。注意,CopyOnWriteArraySet 类在 java.util.concurrent 包中,其他类则在 java.util 包中。还要注意,java.util.BitSet 类没有实现 Set 接口,这个类过时了,用于紧凑而高效地表示布尔值组成的列表,但不是 Java 集合框架的一部分。

实现Set接口的类
SortedSet(接口)

SortedSet 接口提供了多个有趣的方法,这些方法都考虑到了元素是有顺序的,如下述代码所示:

public static void testSortedSet(String[] args) {
    // 创建一个SortedSet对象
    SortedSet<String> s = new TreeSet<>(Arrays.asList(args));

    // 迭代集:元素已经自动排序
    for (String word : s) {
        System.out.println(word);
    }

    // 特定的元素
    String first = s.first(); // 第一个元素
    String last = s.last();   // 最后一个元素

    // 除第一个元素之外的其他所有元素
    SortedSet<String> tail = s.tailSet(first + '\0');
    System.out.println(tail);

    // 除最后一个元素之外的其他所有元素
    SortedSet<String> head = s.headSet(last);
    System.out.println(head);

    SortedSet<String> middle = s.subSet(first+'\0', last);
    System.out.println(middle);
}

必须加上 \0 字符,因为 tailSet() 等方法要使用某个元素后面的元素,对字符串来说,要在后面加上 NULL 字符(对应于 ASCII 中的 0)。

TreeSet(类)

TreeSet 类使用红黑树数据结构维护集,这个集中的元素按照 Comparable 对象的自然顺序升序迭代,或者按照 Comparator 对象指定的顺序迭代。其实,TreeSet 实现的是 Set 的子接口,SortedSet 接口。

TreeSet排序

  • 第一种方式: 需要比较的对象实现Comparable接口,覆盖int compareTo()方法,让元素自身具备比较性
  • 第二种方式:构造实现java.util.Comparator接口,覆盖int compare(T o1, T o2)方法,将比较器对象作为参数传递给TreeSet集合的构造函数.
HashSet(线程不同步)
  • 底层数据结构是哈希表,线程非同步.
  • 通过hasHashCode()和equals()来完成
  • 如果元素的HashCode相同,才会判断equals是否为true
  • 如果元素的HashCode不同,不会调用equals,直接是不等.

注意,对于判断元素是否存在,以及删除等操作,依赖的方法依次是hashcode和equals方法. 在使用HashSet,一定要覆盖int hashCode()和boolean equals (Object obj)方法.

Map接口

将键映射到值的对象,一对一对往里存,而且要保证键的唯一性.

映射(map)是一系列键值对,一个键对应一个值。Map 接口定义了用于定义和查询映射的 API。Map 接口属于 Java 集合框架,但没有扩展 Collection 接口,因此 Map 只是一种集合,而不是 Collection 类型。Map 是参数化类型,有两个类型变量。类型变量 K 表示映射中键的类型,类型变量 V 表示键对应的值的类型。例如,如果有个映射,其键是 String 类型,对应的值是 Integer 类型,那么这个映射可以表示为 Map<String,Integer>。

Map 接口定义了几个最有用的方法:put() 方法定义映射中的一个键值对,get() 方法查询指定键对应的值,remove() 方法把指定的键及对应的值从映射中删除。一般来说,实现 Map 接口的类都要能高效执行这三个基本方法:一般应该运行在常数时间中,而且绝不能比在对数时间中运行的性能差。

Map 的重要特性之一是,可以视作集合。虽然 Map 对象不是 Collection 类型,但映射的键可以看成 Set 对象(因此不能有重复元素。),映射的值可以看成 Collection 对象,而映射的键值对可以看成由 Map.Entry 对象组成的 Set 对象。(Map.Entry 是 Map 接口中定义的嵌套接口,表示一个键值对。)

添加               
    put(K key, V value)               
    putAll(Map<? extends K, ? extends V> m)         
删除                 
    clear()               
    remove(Object key)          
判断               
    containsKey(Object key)               
    containsValue(Object value)               
    isEmpty()          
获取               
    get(Object key)               
    size()               
    values()               
    entrySet()               
    keySet()

下述示例代码展示了如何使用 get()、put() 和 remove() 等方法操作 Map 对象,还演示了把 Map 对象视作集合后的一些常见用法:

// 新建一个空映射
Map<String,Integer> m = new HashMap();

// 不可变的映射,只包含一个键值对
Map<String,Integer> singleton = Collections.singletonMap("test", -1);

// 注意,极少使用下述句法显式指定通用方法emptyMap()的参数类型
// 得到的映射不可变
Map<String,Integer> empty = Collections.<String,Integer>emptyMap();

// 使用put()方法填充映射,把数组中的元素映射到元素的索引上
String[] words = { "this", "is", "a", "test" };
for(int i = 0; i < words.length; i++) {
    m.put(words[i], i); // 注意,int会自动装包成Integer
}

// 一个键只能映射一个值
// 不过,多个键可以映射同一个值
for(int i = 0; i < words.length; i++) {
    m.put(words[i].toUpperCase(), i);
}

// putAll()方法从其他映射中复制键值对
m.putAll(singleton);

// 使用get()方法查询映射
for(int i = 0; i < words.length; i++) {
    if (m.get(words[i]) != i) throw new AssertionError();
}

// 测试映射中是否有指定的键和值
m.containsKey(words[0]);       // true
m.containsValue(words.length); // false

// 映射的键、值和键值对都可以看成集合
Set<String> keys = m.keySet();
Collection<Integer> values = m.values();
Set<Map.Entry<String,Integer>> entries = m.entrySet();

// 映射和上述几个集合都有有用的toString()方法
System.out.printf("Map: %s%nKeys: %s%nValues: %s%nEntries: %s%n",
                  m, keys, values, entries);

// 可以迭代这些集合
// 多数映射都没定义迭代的顺序(不过SortedMap定义了)
for(String key : m.keySet()) System.out.println(key);
for(Integer value: m.values()) System.out.println(value);

// Map.Entry<K,V>类型表示映射中的一个键值对
for(Map.Entry<String,Integer> pair : m.entrySet()) {
    // 打印键值对
    System.out.printf("'%s' ==> %d%n", pair.getKey(), pair.getValue());
    // 然后把每个Entry对象的值增加1
    pair.setValue(pair.getValue() + 1);
}

// 删除键值对
m.put("testing", null);   // 映射到null上可以“擦除”一个键值对
m.get("testing");         // 返回null
m.containsKey("testing"); // 返回true:键值对仍然存在
m.remove("testing");      // 删除键值对
m.get("testing");         // 还是返回null
m.containsKey("testing"); // 这次返回false

// 也可以把映射视作集合,然后再删除
// 不过,向集合中添加键值对时不能这么做
m.keySet().remove(words[0]); // 等同于m.remove(words[0]);

// 删除一个值为2的键值对——这种方式一般效率不高,用途有限
m.values().remove(2);
// 删除所有值为4的键值对
m.values().removeAll(Collections.singleton(4));
// 只保留值为2和3的键值对
m.values().retainAll(Arrays.asList(2, 3));

// 还可以通过迭代器删除
Iterator<Map.Entry<String,Integer>> iter = m.entrySet().iterator();
while(iter.hasNext()) {
    Map.Entry<String,Integer> e = iter.next();
    if (e.getValue() == 2) iter.remove();
}

// 找出两个映射中都有的值
// 一般来说,addAll()和retainAll()配合keySet()和values()使用,可以获取交集和并集
Set<Integer> v = new HashSet<Integer>(m.values());
v.retainAll(singleton.values());

// 其他方法
m.clear();                // 删除所有键值对
m.size();                 // 返回键值对的数量:目前为0
m.isEmpty();              // 返回true
m.equals(empty);          // true:实现Map接口的类覆盖了equals()方法
实现Map接口的类

java.util.concurrent 包中的 ConcurrentHashMap 和 ConcurrentSkipListMap 两个类实现了同一个包中的 ConcurrentMap 接口。ConcurrentMap 接口扩展 Map 接口,而且定义了一些对多线程编程很重要的原子操作方法。例如,putIfAbsent() 方法,它的作用和 put() 方法类似,不过,仅当指定的键没有映射到其他值上时,才会把键值对添加到映射中。

TreeMap 类实现 SortedMap 接口。这个接口扩展 Map 接口,添加了一些方法,利用这种映射的有序特性。SortedMap 接口和 SortedSet 接口非常相似。firstKey() 和 lastKey() 方法分别返回 keySet() 所得集的第一个和最后一个键。而 headMap()、tailMap() 和 subMap() 方法都返回一个新映射,由原映射特定范围内的键值对组成。

Map集合的共性方法注意
  1. 添加元素,如果出现相同的键,那么后添加的值会覆盖原有键对应的值, put方法会会返回被覆盖的值
  2. 可通过get方法的返回值来判断一个键是否存在,通过返回null判断.
  3. 获取map集合中所有的值

两个重要的获取方法: keySet()和entrySet()

  1. 通过keyset()获取key的Set集合,然后Iterator获取key,最终get(Object key) 获取.
  2. 通过entryset()获取关系,然后Iterator获取键值对,最终Map.Entry的getKey和getValue方法获取. (其实Map.Entry也是一个接口,它是Map接口中的一个内部接口)

Map和Set很像,其实Set底层就是使用了Map集合.

练习TreeMap

  • key: 学生Student, value: 地址addr
  • 学生属性:姓名和年龄,注意姓名和年龄相同视为同一个学生,需保证学生的唯一性


Queue接口和BlockingQueue接口

队列(queue)是一组有序的元素,提取元素时按顺序从队头读取。队列一般按照插入元素的顺序实现,因此分成两类:先进先出(first-in, first-out,FIFO)队列和后进先出(last-in, first-out,LIFO)队列。

LIFO 队列也叫栈(stack),Java 提供了 Stack 类,但强烈不建议使用——应该使用实现 Deque 接口的类。

队列也可以使用其他顺序:优先队列(priority queue)根据外部 Comparator 对象或 Comparable 类型元素的自然顺序排序元素。与 Set 不同的是,Queue 的实现往往允许出现重复的元素。而与 List 不同的是,Queue 接口没有定义处理任意索引位元素的方法,只有队列的头一个元素能访问。Queue 的所有实现都要具有一个固定的容量:队列已满时,不能再添加元素。类似地,队列为空时,不能再删除元素。很多基于队列的算法都会用到满和空这两个状态,所以 Queue 接口定义的方法通过返回值表明这两个状态,而不会抛出异常。具体而言,peek() 和 poll() 方法返回 null 表示队列为空。因此,多数 Queue 接口的实现不允许用 null 作元素。

阻塞式队列(blocking queue)是一种定义了阻塞式 put() 和 take() 方法的队列。put() 方法的作用是把元素添加到队列中,如果需要,这个方法会一直等待,直到队列中有存储元素的空间为止。而 take() 方法的作用是从队头移除元素,如果需要,这个方法会一直等待,直到队列中有元素可供移除为止。阻塞式队列是很多多线程算法的重要组成部分,因此 BlockingQueue 接口(扩展 Queue 接口)在 java.util.concurrent 包中定义。

队列不像集、列表和映射那么常用,只在特定的多线程编程风格中会用到。这里,我们不举实例,而是试着厘清一些令人困惑的队列插入和移除操作。

1. 把元素添加到队列中
add()方法
这个方法在 Collection 接口中定义,只是使用常规的方式添加元素。对有界的队列来说,如果队列已满,这个方法可能会抛出异常。

offer()方法
这个方法在 Queue 接口中定义,但是由于有界的队列已满而无法添加元素时,这个方法返回 false,而不会抛出异常。
BlockingQueue 接口定义了一个超时版 offer() 方法,如果队列已满,会在指定的时间内等待空间。这个版本和基本版一样,成功插入元素时返回 true,否则返回 false。

put()方法
这个方法在 BlockingQueue 接口中定义,会阻塞操作:如果因为队列已满而无法插入元素,put() 方法会一直等待,直到其他线程从队列中移除元素,有空间插入新元素为止。

2. 把元素从队列中移除
remove()方法
Collection 接口中定义了 remove() 方法,把指定的元素从队列中移除。除此之外,Queue接口还定义了一个没有参数的 remove() 方法,移除并返回队头的元素。如果队列为空,这个方法会抛出 NoSuchElementException 异常。

poll()方法
这个方法在 Queue 接口中定义,作用和 remove() 方法类似,移除并返回队头的元素,不过,如果队列为空,这个方法会返回 null,而不抛出异常。
BlockingQueue 接口定义了一个超时版 poll() 方法,在指定的时间内等待元素添加到空队列中。

take()方法
这个方法在 BlockingQueue 接口中定义,用于删除并返回队头的元素。如果队列为空,这个方法会等待,直到其他线程把元素添加到队列中为止。

drainTo()方法
这个方法在 BlockingQueue 接口中定义,作用是把队列中的所有元素都移除,然后把这些元素添加到指定的 Collection 对象中。这个方法不会阻塞操作,等待有元素添加到队列中。这个方法有个变体,接受一个参数,指定最多移除多少个元素。

3. 查询
就队列而言,“查询”的意思是访问队头的元素,但不将其从队列中移除。
element()方法
这个方法在 Queue 接口中定义,其作用是返回队头的元素,但不将其从队列中移除。如果队列为空,这个方法抛出 NoSuchElementException 异常。

peek()方法
这个方法在 Queue 接口中定义,作用和 element() 方法类似,但队列为空时,返回 null。

使用队列时,最好选定一种处理失败的方式。例如,如果想在操作成功之前一直阻塞,应该选择 put() 和 take() 方法;如果想检查方法的返回值,判断操作是否成功,应该选择 offer() 和 poll() 方法。

LinkedList 类也实现了 Queue 接口,提供的是无界 FIFO 顺序,插入和移除操作需要常数时间。LinkedList 对象可以使用 null 作元素,不过,当列表用作队列时不建议使用 null。

java.util 包中还有另外两个 Queue 接口的实现。一个是 PriorityQueue
类,这种队列根据Comparator 对象排序元素,或者根据 Comparable
类型元素的 compareTo() 方法排序元素。PriorityQueue 对象的队头始终是根据指定排序方式得到的最小值。另外一个是 ArrayDeque类,实现的是双端队列,一般在需要用到栈的情况下使用。

java.util.concurrent 包中也包含一些 BlockingQueue 接口的实现,目的是在多线程编程环境中使用。有些实现很高级,甚至无需使用同步方法。

实用方法

java.util.Collections 类定义了一些静态实用方法,用于处理集合。其中有一类方法很重要,是集合的包装方法:这些方法包装指定的集合,返回特殊的集合。包装集合的目的是把集合本身没有提供的功能绑定到集合上。包装集合能提供的功能有:线程安全性、写保护和运行时类型检查。包装集合都以原来的集合为后盾,因此,在包装集合上调用的方法其实会分派给原集合的等效方法完成。这意味着,通过包装集合修改集合后,改动也会体现在原集合身上;反之亦然。

第一种包装方法为包装的集合提供线程安全性。java.util 包中的集合实现,除了过时的 Vector 和 Hashtable 类之外,都没有 synchronized 方法,不能禁止多个线程并发访问。如果需要使用线程安全的集合,而且不介意同步带来的额外开销,可以像下面这样创建集合:

List<String> list =
    Collections.synchronizedList(new ArrayList<String>());
Set<Integer> set =
    Collections.synchronizedSet(new HashSet<Integer>());
Map<String,Integer> map =
    Collections.synchronizedMap(new HashMap<String,Integer>());

第二种包装方法创建的集合对象不能修改底层集合,得到的集合是只读的,只要试图修改集合的内容,就会抛出 UnsupportedOperationException 异常。如果要把集合传入方法,但不允许修改集合,也不允许使用任何方式改变集合的内容,就可以使用这种包装集合:

List<Integer> primes = new ArrayList<Integer>();
List<Integer> readonly = Collections.unmodifiableList(primes);
// 可以修改primes列表
primes.addAll(Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19));
// 但不能修改只读的包装集合
readonly.add(23); // 抛出UnsupportedOperationException异常

java.util.Collections 类还定义了用来操作集合的方法。其中最值得关注的是排序和搜索集合元素的方法:

Collections.sort(list);
// 必须先排序列表中的元素
int pos = Collections.binarySearch(list, "key");

Collections 类中还有些方法值得关注:

// 把list2中的元素复制到list1中,覆盖list1
Collections.copy(list1, list2);
// 使用对象o填充list
Collections.fill(list, o);
// 找出集合c中最大的元素
Collections.max(c);
// 找出集合c中最小的元素
Collections.min(c);

Collections.reverse(list); // 反转列表
Collections.shuffle(list); // 打乱列表

Collections.addAll(c, elements ...) // 添加元素

你最好全面熟悉 Collections 和 Arrays 类中的实用方法,这样遇到常见任务时就不用自己动手实现了。

特殊的集合
除了包装方法之外,java.util.Collections 类还定义了其他实用方法,一些用于创建只包含一个元素的不可变集合实例,一些用于创建空集合。singleton()、singletonList() 和 singletonMap() 方法分别返回不可变的 Set、List 和 Map 对象,而且只包含一个指定的对象或键值对。如果要把单个对象当成集合传入方法,可以使用这些方法。

Collections 类还定义了一些返回空集合的方法。如果你编写的方法要返回一个集合,遇到没有返回值的情况时,一般最好返回空集合,而不要返回 null 等特殊的值:

Set<Integer> si = Collections.emptySet();
List<String> ss = Collections.emptyList();
Map<String,Integer> m = Collections.emptyMap();

最后还有个 nCopies() 方法。这个方法返回一个不可变的 List 对象,包含指定数量个指定对象的副本:
List<Integer> tenzeros = Collections.nCopies(10, 0);

数组和辅助方法
由对象组成的数组和集合的作用类似,而且二者之间可以相互转换:

String[] a ={ "this", "is", "a", "test" }; // 一个数组
// 把数组当成大小不可变的列表
List<String> l = Arrays.asList(a);
// 创建一个大小可变的副本
List<String> m = new ArrayList<String>(l);

// asList()是个变长参数方法,所以也可以这么做:
Set<Character> abc = new HashSet<Character>(Arrays.asList('a', 'b', 'c'));
// Collection接口定义了toArray()方法。不传入参数时,这个方法创建
// Object[]类型的数组,把集合中的元素复制到数组中,然后返回这个数组
// 把set中的元素存入数组
Object[] members = set.toArray();
// 把list中的元素存入数组
Object[] items = list.toArray();
// 把map的键存入数组
Object[] keys = map.keySet().toArray();
// 把map的值存入数组
Object[] values = map.values().toArray();

// 如果不想返回Object[]类型的值,可以把一个所需类型的数组传入toArray()方法
// 如果传入的数组不够大,会再创建一个相同类型的数组
// 如果传入的数组太大,复制集合元素后剩余的位置使用null填充
String[] c = l.toArray(new String[0]);

除此之外,还有一些有用的辅助方法,用于处理 Java 数组。为了完整性,这里也会介绍。

java.lang.System 类定义了一个 arraycopy() 方法,作用是把一个数组中的指定元素复制到另一个数组的指定位置。这两个数组的类型必须相同,甚至可以是同一个数组:

char[] text = "Now is the time".toCharArray();
char[] copy = new char[100];
// 从text的第4个元素开始,复制10个字符到copy中
// 这10个字符的位置从copy[0]开始
System.arraycopy(text, 4, copy, 0, 10);

// 把一些元素向后移,留出位置插入其他元素
System.arraycopy(copy, 3, copy, 6, 7);

Arrays 类还定义了一些有用的静态方法:

int[] intarray = new int[] { 10, 5, 7, -3 }; // 由整数组成的数组
Arrays.sort(intarray);                       // 原地排序数组
// 在索引2找到值7
int pos = Arrays.binarySearch(intarray, 7);
// 未找到:返回负数
pos = Arrays.binarySearch(intarray, 12);

// 由对象组成的数组也能排序和搜索
String[] strarray = new String[] { "now", "is", "the", "time" };
Arrays.sort(strarray);  // 排序的结果:{ "is", "now", "the", "time" }

// Arrays.equals()方法比较两个数组中的所有元素
String[] clone = (String[]) strarray.clone();
boolean b1 = Arrays.equals(strarray, clone); // 是的,两个数组相等

// Arrays.fill()方法用于初始化数组的元素
// 一个空数组,所有元素都是0
byte[] data = new byte[100];
// 把元素都设为-1
Arrays.fill(data, (byte) -1);
// 把第5-9个元素设为-2
Arrays.fill(data, 5, 10, (byte) -2);

在 Java 中,数组可以视作对象,也可以按照对象的方法处理。假如有个对象 o,可以使用类似下面的代码判断这个对象是否为数组。如果是,则可以判断是什么类型的数组:

public static void main(String args[]) {        
    Class type = args.getClass();
    if (type.isArray()) {
      Class elementType = type.getComponentType();
      System.out.println(elementType == String.class);
    }
}

小知识

List如何在遍历时删除元素

当尝试用 foreach 或者 Iterator 遍历集合时进行删除或者插入元素的操作时,会抛出这样的异常:java.util.ConcurrentModificationException

关于这个异常的原因,看了很多文章,基本上解释如下:ArrayList的父类AbstarctList中有一个域 modCount,每次对集合进行修改(增添、删除元素)时 modCount 都会+1。
而 foreach 的实现原理就是迭代器 Iterator,在这里,迭代ArrayList的Iterator中有一个变量 expectedModCount,该变量会初始化和 modCount 相等,但当对集合进行插入,删除操作,modCount 会改变,就会造成expectedModCount != modCount,此时就会抛出 java.util.ConcurrentModificationException异常

结论:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容