集合的组成
实际上只有四个基本的集合组件: Map , List , Set 和 Queue ,它们各有两到三个实现版本(Queue 的 java.util.concurrent 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。
虚线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的虚线表示特定的类实现了一个接口。实心箭头表示某个类可以生成箭头指向的类的对象。例如,任何 Collection 都可以生成 Iterator , List 可以生成 ListIterator (也能生成普通的 Iterator ,因为 List 继承自 Collection )。
泛型和类型安全的集合
当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型:
import java.util.*;
class Apple {
private static long counter;
private final long id = counter++;
public long id() { return id; }
}
class GrannySmith extends Apple {}
class Gala extends Apple {}
class Fuji extends Apple {}
class Braeburn extends Apple {}
public class GenericsAndUpcasting {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
apples.add(new GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn());
for(Apple apple : apples)
System.out.println(apple);
}
}
/* Output:
GrannySmith@15db9742
Gala@6d06d69c
Fuji@7852e922
Braeburn@4e25154f
*/
Java里面的泛型其实是使用了语法糖,详情查看之前的文章《深入理解Java虚拟机》-- 语法糖
基本概念
Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:
- 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将
数字和对象关联在一起
。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array)
,因为它将对象和其它对象关联在一起,或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 Map 是强大的编程工具。
List<Apple> apples = new LinkedList<>();
因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口。
这种方式并非总是有效的,因为某些具体类有额外的功能。例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。
列表List
Lists承诺将元素保存在特定的序列中。 List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
有两种类型的 List :
- 基本的 ArrayList ,
擅长随机访问元素
,但在 List 中间插入和删除元素时速度较慢。 - LinkedList ,它通过代价较低的在 List 中间进行的
插入和删除操作
,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。
LinkedList和ArrayList都实现了List接口,但LinkedList底层是双向链表,所以不存在索引。
查询:LinkedList根据传入进来的节点位置,与集合尺寸 size/2的结果进行比较,再决定从链表头部或者链表尾部遍历查询所有节点,所以查询较慢。ArrayList是基于数组实现,可以根据下标直接获得位置。
插入和删除:
- LinkedList在插入时需要向移动指针到指定节点,才能开始插入,一旦要插入的位置比较远,LinkedList就需要一步一步的移动指针,直到移动到插入位置;删除时,只需要前后两个节点改变一下指针的指向,并把要删除的节点置为null即可,不需要改变元素位置,所以删除较快。(图中所有的地址,表示节点的地址)
- ArrayList是基于数组实现,可以根据下标直接获得位置, 这省去了查找特定节点的时间,数组实现的要在x处添加一个数据,x后的节点都要后移一位,属于代价高昂的操作。
遍历:
如果只是想向前遍历 List ,并不打算修改 List 对象本身,那么使用for-in
语法更加简洁。
不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用
Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:
以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
集合Set
Set 不保存重复的元素。 如果试图将相同对象的多个实例添加到 Set 中,那么它会阻止这种重复行为。 Set 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 Set 中。因此,查找通常是 Set 最重要的操作,因此通常会选择 HashSet 实现,该实现针对快速查找进行了优化。
由 HashSet 维护的顺序与 TreeSet 或 LinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。
TreeSet 将元素存储在红-黑树数据结构
中,而 HashSet 使用散列函数
。 LinkedHashSet 因为查询速度的原因也使用了散列
,但是看起来使用了链表来维护元素的插入顺序
。看起来散列算法好像已经改变了,现在 Integer 按顺序排序。
Set最常见的操作之一是使用 contains()
测试成员归属性,Set列表中通过如果想按字母顺序
// collections/UniqueWordsAlphabetic.java
// Producing an alphabetic listing
import java.util.*;
import java.nio.file.*;
public class UniqueWordsAlphabetic {
public static void
main(String[] args) throws Exception {
List<String> lines = Files.readAllLines(
Paths.get("SetOperations.java"));
Set<String> words =
new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for(String line : lines)
for(String word : line.split("\\W+"))
if(word.trim().length() > 0)
words.add(word);
System.out.println(words);
}
}
/* Output:
[A, add, addAll, added, args, B, C, class, collections,
contains, containsAll, D, E, F, false, from, G, H,
HashSet, I, import, in, J, java, K, L, M, main, N, new,
out, Output, println, public, remove, removeAll,
removed, Set, set1, set2, SetOperations, split, static,
String, System, to, true, util, void, X, Y, Z]
*/
映射Map
Map 与数组和其他的 Collection 一样,可以轻松地扩展到多个维度,只需要创建一个值为 Map 的 Map(这些 Map 的值可以是其他集合,甚至是其他 Map)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。
各个实现类的特点
- HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
- Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
- LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
- TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
队列Queue
队列是一个典型的“先进先出”(FIFO)集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。