Java 集合的使用

      • 集合概述与迭代器
        • 迭代器
        • ListIterator
        • ConcurrentModification
      • 具体的集合
        • 数组列表
        • 链表
        • 散列集
        • 树集
        • 队列
        • 优先级队列
        • 映射表
        • LinkedHash
        • EnumSet / EnumMap
      • 集合框架
        • 集合中的接口
        • 集合中的类
        • 视图与包装器
          • 子范围
          • 不可修改的视图
          • 同步视图
        • 集合与数组之间的转换
        • 参考

集合概述与迭代器

Java 集合类库将接口和实现分离。

在 Java 类库中,集合类的基本接口是 Collection 接口。它继承了 Iteratable 接口,因此 Collection 接口有一个 iterator 方法:

public interface Collection<E> extends Iterable<E> {
    Iterator iterator();
}

对任何扩展了 Collection 的集合类,都可以使用迭代器对元素进行访问。

迭代器

Iterator 接口有三个主要方法:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

迭代器的使用方法是这样的:

Collection c = new ArrayList<>();
Iterator iter = c.iterator();
while(iter.hasNext()){
  String str = iter.next();
  System.out.println(str);
}

还可以增强 for 循环进行遍历:

for(String str : c){
  System.out.println(str);
}

编译器简单地将这种 for 循环翻译为带有迭代器的循环。

Iterator 是 1.2版本引进来的,它的 next 和 hasNext 方法 与 1.0 版本中的 Enumeration 接口的 nextElement 和 hasMoreElements 方法的作用一样。Iterator 有两个优点:

  1. 增加了 remove 方法,可以在迭代的时候,删除元素;
  2. 命名更加简洁。

Java 中的迭代器与其他语言中的迭代器的区别:

在传统的集合类库中,例如,C++ 的标准模版库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引 i 就可以查看数组元素 a[i] 一样。不需要查找元素,就可以将迭代器向前移动一个位置。这与不需要执行查找操作就可以通过 i++ 将数组索引向前移动一样。

但是,java 迭代器并不是这样的。查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next,而在执行查找操作的同时,迭代器的位置随之向前移动。

因此,应该将 Java 迭代器认为是位于两个元素之间。当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

——《Java 核心技术》

Java 集合的使用_第1张图片

Iterator 的 remove 方法会删除上次调用 next 方法返回的元素,每次调用 remove 之前,必须调用 next 方法。下面是如何删除集合中第一个元素的方法:

Iterator iter = c.iterator();
iter.next();
it.remove();    

ListIterator

ListIterator 是 Iterator 的子接口,ListIterator 不仅可以向后迭代,也可以向前迭代。相比 Iterator,它增加了以下这些方法:

boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);

有序的集合的 listIterator 方法返回了 ListIterator 迭代器。

add 方法在迭代器位置之前添加一个对象。不像 remove 方法那样,add 方法可以连续调用。

set 方法用一个新元素取代调用 next 或 previous 方法返回的上一个元素。

ListIterator iter = staff.listIterator();
iter.next();    // 获取第一个值
iter.set(newValue); // 取代第一个值

ConcurrentModification

在某个迭代器修改集合时,另一个迭代器对其进行遍历,就有可能出现ConcurrentModificationException。例如:

Map map = new HashMap<>();
map.put("monday","编程学习");
map.put("tuesday","健身");
map.put("wednesday","跑步");

Set key = map.keySet();

Iterator iter1 = key.iterator();
Iterator iter2 = key.iterator();
iter1.next();
iter1.remove(); //删除第一个元素
iter2.next(); // throws ConcurrentModificationException

根据集合是否时线程安全的,它们返回的迭代器也有所不同。

像 ArrayList、HashMap、Set 等线程不安全的集合,它们返回的迭代器是 Fail-fast iterators。它们遍历集合自身的元素,当有并发修改时,就会抛出 ConcurrentModificationException。就像上面的例子。

像 CopyOnWriteArrayList、ConcurrentHashMap 这样线程安全的集合,它们返回的迭代器是 Fail-safe iterators。它们会复制一份集合的元素,保证并发修改的安全。缺点是内存消耗多,遍历的元素获得的数据可能是老数据。

将上面例子改成用 ConcurrentHashMap,在最后调用 iter2.next(); 的时候,就不会抛异常。

具体的集合

数组列表

List 接口用于描述一个有序集合。有两种方式可以访问元素:一种是用迭代器,另一种是用 get 和 set 方法随机地访问每个元素。前者适用于链表,后者使用与数组列表。

ArrayList 就是这样的结构,它封装了一个动态再分配的对象数组。缺陷就是在中间位置删除或者插入一个元素比较费劲,因为其后的所有元素都要移动。

Vector 也是这样的结构,在 java 第一个版本就有 Vector。它的所有方法都是同步的,多个线程可以安全地访问一个 Vector 对象。

CopyOnWriteArrayList 是另一个线程安全的 List,java 1.5 加入,效率比 Vector 高。

链表

链表将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用。在java中,所有链表都是双向链表——即每个节点还存放着上一个节点的引用。

很轻松地就可以从链表中删除一个元素,只需要对被删除元素附近的结点更新一个即可。

Java 集合的使用_第2张图片

LinkedList 的数据结构就是链表,使用它的 listIterator 方法可以获得 ListIterator 迭代器,从而对链表进行操作。

如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,就可能出现 Concurrent ModificationException 异常。因此,多个迭代器同时操作时,一定要注意。

链表不支持快速地随机访问。如果要查看链表中第 n 个元素,必须从头开始,越过 n-1 个元素。如果需要从用整数索引访问元素时,尽量不用链表。

尽管如此,LinkedList 还是提供了 get 方法用来访问特定元素,只是这个方法的效率并不太高。

绝对不能使用下面的方法遍历链表,效率极低:

for(int i = 0; i< list.size(); i++){
    System.out.println(list.get(i));
}

get 方法做了微小的优化,如果索引大于 size()/2 就从列表尾端开始搜索元素。

列表迭代器的 nextIndex 和 previousIndex 方法,可以返回当前位置的索引。

list.listIterator(n) 将返回一个迭代器,它指向索引为 n 的元素前面的位置。也就是说,调用 next 与调用 list.get(n) 会获取到同一个元素。这个迭代器的效率比较低。

如果链表中的元素比较少,就不必担心 get 和 set 方法的开销。使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付的代价。如果只有少数几个元素,可以使用 ArrayList 存储。

散列集

散列表为每个对象计算一个整数,成为散列码(hash code),每个对象的 hash code 是不一样的。如果要自定义类,就要负责实现 hasCode 方法。注意,自己实现的 hashCode 方法 和 equals 方法应该兼容,即 a.equals(b) 为 true,那么 a 和 b 的 hasCode 应该一样。

在 Java 中,hast table 用链表数组实现。每个列表被称为桶(bucket)。想要查找表中对象的位置,就要先计算它的 hash code,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的 hash code 为 76268,并且有 128 个桶,对象应该保存在第 108 号桶中。

如果这个桶中没有其他元素,这个对象可以直接插入桶中。如果桶被占了,这种情况被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
Java 集合的使用_第3张图片

如果想要更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。

如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的 75%~150%。标准类库使用的桶数是 2 的幂,默认值是 16。

如果散列表太满,就需要再散列(rehashed),即创建一个双倍桶数的表,然后将所有的元素插入到这个新表,丢弃原来的表。装填因子(load factor)决定何时再散列。默认是 0.75,即表中超过 75% 的位置已经填入元素,就进行再散列。

散列表迭代器将依次访问所有的桶,由于散列表将元素分散在表的各个位置上,所以访问它们的顺序是随机的。

Java 集合类库中的 set 类型就是散列表的数据结构,它没有重复元素,每次在添加对象之前,都会查找是否在已经存在。HashSet 实现了基于散列表(hash table)的集。

HashMap 中的 key 的结构也是散列集。

在创建 HashSet 和 HashMap 的时候,最好预计一下容量。如果容量不足,要再散列的话,就有点影响性能了。

在 Guava 库中,Maps.newHashMapWithExpectedSize()Sets.newHashSetWithExpectedSize() 这两个方法,就是创建指定大小的 HashMap 和 HashSet。

树集

有序集合,底层结构是红黑树(red-black tree)。每次插入元素,会按照排序放在正确的位置。元素的访问顺序也是排序的顺序,而不是插入的顺序。

树集假定插入的元素实现了 Comparable 接口,这个接口定义了一个方法:

public int compareTo(T o);

如果 a 与 b 相等,返回 a.compareTo(b) 返回 0;如果返回正数,表示 a 位于 b 之后;如果返回负数,表示 a 位于 b 之前。

如果在树集中放置自定义的对象,这个对象必须实现 Comparable 接口来自定义排序的方式。

class Student implements Comparable{
  public int compareTo(Student other){
    return age - other.age;
  }
}

另一种方法是将 Comparator 对象传递给 TreeSet 构造器来告诉树集怎么排序。

class SutdentComparator implements Comparator{
  public int compare(Sutdent a, Sutdent b){
    return (a.age).compareTo(b.age);
  }
}

然后将这个类的对象传递给树集的构造器:

SortedSet sortByAge = new TreeSet<>(new SutdentComparator());

或者,直接定义一个匿名内部类传递给 TreeSet:

SortedSet sortByAge = new TreeSet<>(new Comparator() {
  @Override
  public int compare(Sutdent a, Sutdent b){
    return (a.age).compareTo(b.age);
  }
});

队列

可以在队列尾部添加元素,在队列的头部删除元素。

队列通常有两种实现方式:一种是使用循环数据;另一种是使用链表。

Java 集合的使用_第4张图片

如果需要一个循环数组队列,就可以使用 ArrayDeque。如果需要一个链表队列,就可以使用 LinkedList

循环数组要比链表更高效,但它是一个有界集合,即容量有限。如果要收集的对象数量没有上限,就要用链表来实现。

优先级队列

优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方法处理这些元素,并不需要对他们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet 一样,优先级队列既可以保存实现了 Comparable 接口的类对象,也可以保存在构造器中提供比较器的对象。

  • PriorityQueue()
  • PriorityQueue(int initialCapacity)
  • PriorityQueue(int initialCapacity, Comparator

映射表

映射表用来存放键/值对。

Java 类库为映射表提高了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。

HashMap 对键进行散列,TreeMap 用键的整体顺序对元素进行排序,并将其组织成树。这两个都是对进行操作,与键关联的值无关。

映射表有三个视图,分别是:键集、值集合和键/值对集。

  • Set keySet()
  • Collection values()
  • Set> entrySet()

如果想要同时查看键和值,就可以通过枚举各个条目查看:

for(Map.Entry entry : map.entrySet()){
  System.out.println(entry.getKey());
  System.out.println(entry.getValue());
}

如果调用迭代器的 remove 方法,就从映射表中删除了键以及对应的值,但是不能使用它添加元素。

Set> entries = map.entrySet();
entries.remove("12");

Java 1.0 版本中的 Hashtable 与 HashMap相似,只不过 Hashtable 是线程安全的,它的方法仅仅是使用了 synchronized 进行同步,效率不是很高,现在基本上不怎么使用。

ConcurrentHashMap 也是线程安全的,它的性能要比 Hashtable 高很多。

LinkedHash

Java SE 1.4 增加了两个类:LinkedHashSet 和 LinkedHashMap,用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。

Java 集合的使用_第5张图片

LinkedHashMap 将用访问顺序,而不是插入顺序,对映射表条目进行迭代。每次调用 get 或 put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表的中桶不会受影响)。构造一个这样的 LinkedHashMap,要调用:

LinkedHashMap(initialCapacity, loadFactor, true)

访问顺序对于实现高速缓存的“最近最少使用”原则十分重要。例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满了,可以使用迭代器把前面几个元素删掉。这些是近期最少使用的元素。

EnumSet / EnumMap

EnumSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以 EnumSet 内部用位序列实现。如果对应的值在集中,则相应的位被置为 1。

enum WeekDay {
        Monday,
        Tuesday,
        wednesday,
        thursday,
        friday,
        saturday,
        sunday
    }

    public static void main(String[] args) {
        EnumSet allOf = EnumSet.allOf(WeekDay.class);
        EnumSet noneOf = EnumSet.noneOf(WeekDay.class);
        EnumSet workday = EnumSet.range(WeekDay.Monday,WeekDay.friday);
        EnumSet weekend = EnumSet.of(WeekDay.saturday,WeekDay.sunday);
    }

EnumMap 是一个键为枚举类型的映射表。它可以高效地用一个值数组实现。它不需要计算 hash code,也不需解决散列冲突。在使用时,需要在构造器中指定键的类型:

EnumMap dayOfThings = new EnumMap<>(WeekDay.class);

集合框架

集合中的接口

以 Map 结尾的类实现了 Map 接口,其它类都实现了 Collection 接口。

Java 集合的使用_第6张图片

RandomAccess 接口没有任何方法,它可以用来检测一个特定的集合是否支持高效的随机访问:

if (c instanceof RandomAccess){
  // use random access algorithm
}else {
  // use sequential access algorithm
}

ArrayList 和 Vectror 类都实现了 RandomAccess 接口,所有它们可以随机地访问元素。

List 是一个有序结合,元素可重复。List 接口定义了几个可用于随机访问的方法:

E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);

Set 接口的所有方法在 Collection 接口中都有定义,只是 Set 的 add 方法拒绝添加重复的元素。既然在 Collection 接口中都有,为什么还要有 Set 呢?从概念上讲,并不是所有的集合都是集。建立 Set 接口之后,就可以编写仅接口集的方法。就像我们经常使用的:Set sets = new HashSet<>();

SortedSet 和 SortedMap 接口暴露了用于排序的比较器对象,并且定义的方法可以获得集合的子集视图。

Java SE 6 引入了接口 NavigableSet 和 NavigableMap,包含了几个用于在有序集和映射表中查找和遍历的方法。TreeSet 和 TreeMap 类实现了这几个接口。

集合中的类

集合接口中有大量的方法,这些方法可以有通用的实现。在研究 API 文档时,会发现另外一组名字以 Abstract 开头的类,这些类是类库实现者而设计的。如果想要实现自己的集合类,扩展这些 Abstract 类要比实现最上层的接口中的所有方法轻松得多。比如 AbstractCollection 类已经实现了 Collection 接口常用的方法,因此,一个具体的集合类可以直接扩展 AbstratCollection 类,具体的集合类只需要提供 iterator 和 size 方法就可以了。

public abstract class AbstractCollection<E> implements Collection<E> {
    public abstract Iterator iterator();
    public abstract int size(); 
}

Java 集合框架提供了这些抽象类:

  • AbstractCollection
  • AbstractList
  • AbstractSequentialList
  • AbstractSet
  • AbstractQueue
  • AbstractMap

Java 集合的使用_第7张图片

AbstractList 的抽象方法:

abstract public E get(int index);

Java 第一版的一些容器类,已经被集成到集合框架中:

Java 集合的使用_第8张图片

Vector 和 ArrayList 相似,只不过 Vector 是线程安全的。

Hashtable 和 HashMap 相似,只不过 Hashtable 是线程安全的。

视图与包装器

像 Map 的 keySet 方法,它返回一个现实了 Set 接口的类对象,这个用这个对象对 Map 进行操作。这种集合称为视图

Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器,因此它的大小是固定的,改变数组大小的所有方法,例如 add 和 remove,都会抛出一个 UnsupportedOperationException 异常。

String[] strs = new String[3];
List list = Arrays.asList(strs);
// 或者
List list = Arrays.asList("1","2","3");

Collections.nCopies(n, anObject) 将返回一个实现了 List 接口的不可修改的对象。下面的调用将创建一个包含 100 个字符串的 List,每个都是 “test”:

List list = Collections.nCopies(100,"test");

由于字符串对象只存储了一次,所以付出的存储代价很小。这是视图技术的一种巧妙应用。

子范围

可以为很多集合建立子范围视图。例如:

List sub = list.subList(1,3);

对子范围视图的操作,例如 clear、add、remove等,会影响整个列表。

对于有序集和映射表,可以使用排序顺序而不是元素位置建立子范围。SortedSet 接口中获取子范围的方法:

SortedSet subSet(E fromElement, E toElement);
SortedSet headSet(E toElement);
SortedSet tailSet(E fromElement);

SortedMap 接口中获取子范围的方法:

SortedMap subMap(K fromKey, K toKey);
SortedMap headMap(K toKey);
SortedMap tailMap(K fromKey);

Java SE 6 引入的 NavigableSet 和 NavigableMap 接口,在操作子范围的时候,可以指定是否包括边界:

NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive);
NavigableSet headSet(E toElement, boolean inclusive);
NavigableSet tailSet(E fromElement, boolean inclusive);
不可修改的视图

Collections 有几个方法,用于产生集合的不可修改视图,尝试对这些视图做修改,就会抛出 UnsupportedOperationException 异常:

  • Collections.unmodifiableCollection
  • Collections.unmodifiableSet
  • Collections.unmodifiableSortedSet
  • Collections.unmodifiableNavigableSet
  • Collections.unmodifiableList
  • Collections.unmodifiableMap
  • Collections.unmodifiableSortedMap
  • Collections.unmodifiableNavigableMap
同步视图

一些集合并不是线程安全的,如果有多个线程同时对其进行读写操作,就会抛异常。Collections 有一些方法,用于产生集合的同步视图,可以确保集合的线程安全。

  • synchronizedCollection
  • synchronizedSet
  • synchronizedSortedSet
  • synchronizedNavigableSet
  • synchronizedList
  • synchronizedMap
  • synchronizedSortedMap
  • synchronizedNavigableMap

例如 synchronizedMap 方法可以将任何一个 Map 转换成具有同步访问方法的 Map:

Map map = Collections.synchronizedMap(new HashMap<>());

现在就可以用多线程方法 map 对象了。

集合与数组之间的转换

使用 Arrays.asList 可以将数组转换为集合:

String[] values = ...;
HashSet staff = new HashSet<>(Arrays.asList(values));

将集合转成数组,可以用 toArray 方法:

Object[] values = staff.toArray();

注意,toArray 返回的是对象数组,不能进行强制转成特定类型的对象。但可以这样用:

String[] strs =  list.toArray(new String[list.size()]);

参考

  1. 《Java 核心技术》
  2. http://www.baeldung.com/java-collections-interview-questions

你可能感兴趣的:(Java)