文章目录
七、Java集合
7.4 List 集合
List 集合代表一个元素有序、可重复的集合,集合中每个元素都有对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引。
7.4.1 List 接口和 ListIterator 接口
List 作为 Collection 接口的子接口,可以使用 Collection 接口里的全部方法。由于 List 是有序集合,因此 List 集合增加了一些根据索引来操作集合元素的方法。
- void add(int index, Object element):将元素 element 插入到 List 集合的 index 处(即 index 处的元素即为 element);
- boolean addAll(int index, Collection c):将集合 c 所包含的所有元素都插入到 List 集合的 index 处;
- Object get(int index):返回集合 index 索引处的元素;
- int indexOf(Object o):返回对象 o 在 List 集合中第一次出现的位置索引;
- int lastIndexOf(Object o):返回对象 o 在 List 集合中最后一次出现的位置索引;
- Object remove(int index):删除并返回 index 索引处的元素;
- Object set(int index, Object element):将 index 索引处的元素替换成 element 对象,返回被替换的旧元素;
- List subList(int fromIndex, int toIndex):返回从索引 fromIndex(包含)到 toIndex(不包含)处所有集合组成的子集合;
- void replaceAll(UnaryOperator operator):根据 operator 指定的计算规则重新设置 List 集合的所有元素;
- void sort(Comparator c):根据 Comparator 参数对 List 集合的元素排序;
import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List books = new ArrayList();
books.add(new String("Java核心技术"));
books.add(new String("Java编程思想"));
books.add(new String("JVM虚拟机"));
System.out.println(books);
//将新字符串插入到第二个位置
books.add(1, new String("Java性能调优指南"));
//for循环迭代访问books集合
for (int i = 0; i < books.size(); i++) {
System.out.println(books.get(i));
}
//删除第三个元素
books.remove(2);
//判断指定元素在List集合中的位置
System.out.println(books.indexOf(new String("Java性能调优指南")));
//将第二个元素替换成新的字符串对象
books.set(1, new String("Effective Java"));
//将books集合中第二个是元素到第三个元素截取成子集合
System.out.println(books.subList(1, 2));
}
}
注意到,程序试图返回新字符串对象在 List 集合中的位置,实际上 List 集合中并未包含该字符串对象。因为 List 集合添加字符串对象时,添加的是通过 new 关键字创建的新字符串对象,显然两者不是同一对象,但 List 的indexOf
方法依然可以返回 1。List 判断两个对象相等的标准是通过 equals() 方法比较返回 true 即可。
当调用 List 的 set(int index, Object element) 方法来改变 List 集合指定索引处的元素时,指定的索引必须是 List 集合的有效索引
Java 8 为 List 集合增加了sort()
和replaceAll()
两个常用的默认方法,其中 sort() 方法需要一个 Comparator 对象来控制元素排序,程序可使用 Lambda 表达式作为参数;而replaceAll()
方法则需要一个 unaryOperator 来替换所有集合元素,UnaryOperator 也是一个函数式接口,因此程序也可以使用 Lambda 表达式作为参数。
import java.util.ArrayList;
import java.util.List;
public class ListTest2 {
public static void main(String[] args) {
List books = new ArrayList();
books.add(new String("Java核心技术"));
books.add(new String("Java编程思想"));
books.add(new String("JVM虚拟机"));
//使用目标类型为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);//输出[6, 8, 8]
}
}
List 集合进行排序,传给sort()
方法的 Lambda 表达式指定的排序规则:字符串长度越长,字符串越大,因此 List 集合中的字符串会按由短到长的顺序排列。另外replaceAll()
方法的 Lambda 表达式指定了替换集合元素的规则:直接用集合元素(字符串)的长度作为新的集合元素,执行该方法后集合元素被替换字符串长度。
与 Set 只提供一个iterator()
方法不同,List 还额外提供了一个listIterator()
方法,该方法返回一个 ListIterator 对象,ListIterator 接口继承了 Iterator 接口,提供专门操作 List 的方法。ListIterator 接口在 Iterator 接口基础上增加如下方法:
- boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素;
- Object previous():返回该迭代器的上一个元素;
- void add(Object o):在指定位置插入一个元素;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorTest {
public static void main(String[] args) {
String[] books = {"Java核心技术", "Java编程思想", "JVM虚拟机"};
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());
}
}
}
7.4.2 ArrayList 和 Vector 类
ArrayList 和 Vector 作为 List 类的典型实现,完全支持前面介绍的 List 接口的全部功能。ArrayList 和 Vector 类都是基于数组实现的 List 类,所以 ArrayList 和 Vector 类封装了一个动态的、允许再分配的 Object[] 数组。
ArrayList 或 Vector 对象使用 initialCapacity 参数来设置该数组的长度,当向 ArrayList 或 Vector 中添加元素超过该数组的长度时,他们的 intialCapacity 会自动增加。如果向 ArrayList 或 Vector 集合添加大量元素时,可使用 ensureCapacity(int minCapacity) 方法一次性增加 intialCapacity。这样可以减少重分配的次数,从而提高性能。如果创建空的 ArrayList 或 Vector 集合时不指定 initialCapacity 参数,则 Object[] 数组的长度默认为10。除此之外,ArrayList 和 Vector 还提供了如下两个方法来重新分配数组:
- void ensureCapacity(int minCapacity):将 ArrayList 或 Vector 集合的 Object[] 数组长度增加大于或等于 minCapacity 值;
- void trimToSize():调整 ArrayList 或 Vector 集合的 Object[] 数组长度为当前元素的个数。调用该方法可减少 ArrayList 或 Vector 集合对象占用的存储空间;
ArrayList 和 Vector 的显著区别是:ArrayList 是线程不安全的,当多个线程访问同一个 ArrayList 集合时,如果有超过一个线程修改了 ArrayList 集合,则程序必须手动保证该集合的同步性。 Vector 集合则是线程安全的,无须程序保证该集合的同步性,也因此性能比 ArrayList 的性能要低。实际上,即使需要保证 List 集合线程安全,也同样不推荐使用 Vector 实现类。后面会介绍 Collections 工具类,它可以将 ArrayList 变成线程安全的。
Vector 还提供了 Stack 子类,用于模拟栈数据结构,继承 Vector 同样也是线程安全性能较差,因此也应该尽量少用。如果需要使用栈,可以考虑使用 ArrayDeque。ArrayDeque 实现了 List 和 Deque 接口,底层是基于数据的实现,因此性能很好。
7.4.3 固定长度的 List
Arrays 工具类中提供了asList(Object... a)
方法,该方法可以将一个数组或指定个数的对象转换成一个 List 集合,这个 List 集合既不是 ArrayList 实现类的实例,也不是 Vector 实现类的实例,而是 Arrays 的内部类 ArrayList 的实例。
Arrays.ArrayList 是一个固定长度的 List 集合,程序只能遍历访问该集合的元素,不可增加、删除该集合里的元素。
import java.util.Arrays;
import java.util.List;
public class FixedSizeLst {
public static void main(String[] args) {
List fixedList = Arrays.asList("Java编程思想", "Java虚拟机");
//获取fixedList的实现类,将输出Arrays$ArrayList
System.out.println(fixedList.getClass());
//使用方法引用遍历集合元素
fixedList.forEach(System.out::println);
//试图增加或删除元素都会引发UnsupportedOperationException
//fixedList.add("Java核心技术");
//fixedList.remove("Java编程思想");
}
}
7.5 Queue 集合
Queue 用于模拟队列这种数据结构,定义了如下几个方法:
- void add(Object e):将指定元素加入此队列的尾部;
- Object element():获取队列的头部元素,但是不删除元素;
- boolean offer(Object e):将指定元素加入此队列的尾部,当使用有容量限制的队列时,此方法通常比 add(Object e) 方法更好;
- Object peek():获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回 null;
- Object poll():获取队列头部的元素,并删除该元素,如果队列为空,则返回null;
- Object remove():获取队列头部的元素,并删除该元素;
Queue 接口有一个 PriorityQueue 实现类,除此之外,Queue 还有一个 Deque 接口,Deque 代表一个“双端队列”,双端队列可以从两端来添加删除元素,因此 Deque 的实现类既可以当成队列使用,也可以当成栈来使用。Java 为 Deque 提供了 ArrayDeque 和 LinkedList 实现类。
7.5.1 PriorityQueue 实现类
PriorityQueue 是一个比较标准的队列实现类,它保存队列元素的顺序并不按加入队列的顺序,而是按队列元素的大小进行重新排序。因此调用peek()
方法或者poll()
方法取出队列中元素是队列中最小的元素,因此,PriorityQueue 已经违反了队列的基本规则:先进先出(FIFO)。
import java.util.PriorityQueue;
public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue pq = new PriorityQueue();
pq.offer(6);
pq.offer(-3);
pq.add(1);
pq.offer(2);
pq.offer(5);
//输出pq队列,并不是按照元素的加入顺序排列
System.out.println(pq);
int size = pq.size();
for (int i = 0; i < size; i++) {
System.out.println(pq.poll());
}
}
}
直接输出 PriorityQueue 时,可能看到该队列里的元素并没有很好地按大小进行排序,但这只是由于toString()
方法的返回值影响。事实上,程序多次调用 PriorityQueue 集合对象的 poll() 方法,即可看到元素从小到大的顺序“移出队列”。
PriorityQueue 不允许插入 null 元素,它还需要对队列元素进行排序,PriorityQueue 的元素有两种排序方法,对元素的要求与 TreeSet 元素的要求一致:
- 自然排序:采用自然排序的 PriorityQueue 集合中的元素必须实现了 Comparable 接口,而且应该是同一个类的多个实例,否则会导致 ClassCastException 异常;
- 定制排序:创建 PriorityQueue 队列时传入 Comparator 对象,该对象负责对队列中的所有元素进行排序,采用定制排序时不要求队列元素实现 Comparable 接口;
7.5.2 Deque 接口与 ArrayDeque 实现类
Deque 接口是 Queue 接口的子接口,它代表一个双端队列,Deque 接口里定义了一些双端队列的方法,这些方法允许从双端来操作队列的元素:
- void addFirst():将指定元素插入该双端队列的开头;
- void addLast():将指定元素插入该双端队列的末尾;
- Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素;
- Object getFirst():获取但不删除该双端队列的第一个元素;
- Object getLast():获取但不删除该双端队列的最后一个元素;
- boolean offerFirst(Object o):将指定元素插入该双端队列的开头;
- boolean offerLast(Object o):将指定元素插入该双端队列末尾;
- Object peekFirst():获取但不删除该双端队列的第一个元素,如果此双端队列为空,则返回 null;
- Object peekLast():获取但不删除该双端队列的最后一个元素,如果此双端队列为空,则返回 null;
- Object pollFirst():获取并删除该双端队列的第一个元素,如果此双端队列为空,则返回 null;
- Object pollLast():获取并删除该双端队列的最后一个元素,如果此双端队列为空,则返回 null;
- Object pop(栈方法):pop 出该双端队列所表示栈的栈顶元素,相当于 removeFirst();
- void push(Object e)(栈方法):将一个元素 push 进该双端队列所表示的栈的栈顶,相当于 addFirst(e);
- Object removeFirst():获取并删除该双端队列的第一个元素;
- Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素 o;
- Object removeLast():获取并删除该双端队列的最后一个元素;
- Object removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素 o;
Deque 不仅可以当成双端队列来使用,而且可以被当成栈来使用,因为该类里还包含了 pop(出栈)、push(入栈)两个方法。
Queue 方法 | Deque 方法 | Stack 方法 |
---|---|---|
add(e) / offer(e) | addFirst(e) / offerLast(e) | push(e) |
remove() / poll() | removeFirst() / pollFirst() | pop() |
element() / peek() | getFirst() / peekFirst() | peek() |
Deque 接口提供了一个典型的实现类:ArrayDeque,它是基于数组实现的双端队列,创建 Deque 同样可以指定一个 numElements 参数,该参数用于指定 Object[] 数组的长度;如果不指定 numElements 参数,Deque 底层数组的长度为 16。
ArrayList 和 ArrayDeque 两个集合类的实现机制基本相似,它们的底层都采用了一个动态的、可重新分配的 Object[] 数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个 Object[] 数组来存储集合元素。
ArrayDeque 作为栈使用:
import java.util.ArrayDeque;
public class ArrayDequeStack {
public static void main(String[] args) {
ArrayDeque stack = new ArrayDeque();
//元素入栈
stack.push("Java编程思想");
stack.push("Java核心技术");
stack.push("Java虚拟机");
System.out.println(stack);
//访问第一个元素,但并不出栈
System.out.println(stack.peek());
//元素出栈
System.out.println(stack.pop());
System.out.println(stack);
}
}
ArrayDeque 作为队列使用:
import java.util.ArrayDeque;
public class ArrayDequeQueue {
public static void main(String[] args) {
ArrayDeque queue = new ArrayDeque();
//依次将三个元素加入队列
queue.offer("Java编程思想");
queue.offer("Java核心技术");
queue.offer("Java虚拟机");
System.out.println(queue);
//访问队列头部元素,但不出poll队列
System.out.println(queue.peek());
//poll出队列的第一个元素
System.out.println(queue.poll());
System.out.println(queue);
}
}
7.5.3 LinkedList 实现类
LinkedList 类时 List 接口的实现类,这意味着它是一个 List 集合,可以根据索引来随机访问集合中的元素。除此之外,LinkList 还实现了 Deque 接口,可以被当成双端队列来使用,因此既可以被当成“栈”来使用,也可以被当成队列来使用。
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList books = new LinkedList();
//将字符串元素加入队列的尾部
books.offer("Java编程思想");
//将字符串元素添加到栈的顶部
books.push("Java核心技术");
//将字符串元素添加到队列的头部(相当于栈的顶部)
books.addFirst("Java虚拟机");
//以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.pollFirst());
System.out.println(books);
//访问并删除队列最后一个元素
System.out.println(books.pollLast());
System.out.println(books);
}
}
LinkedList 与 ArrayList、ArrayDeque 的实现机制完全不同,ArrayList、ArrayDeque 内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而 LinkedList 内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能出色(只需要改变指针所指向的地址即可)。需要指出的是,Vector 也是以数组的形式来存储集合元素的,但它实现了线程同步功能(实现机制也不好),所以各方面性能比较差。
对于所有的内部基于数组的集合实现,例如 ArrayList、ArrayDeque 等,使用随机访问的性能比使用 Iterator 迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。
7.5.4 各种线性表的性能分析
Java 提供的 List 就是一个线性表接口,而 ArrayList、LinkedList 又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue 代表了队列,Deque 代表了双端队列(既可以作为队列使用,也可以作为栈使用)
使用 List 集合有以下建议:
- 如果需要遍历 List 集合元素,对于 ArrayList、Vector 集合,应该使用随机访问方法(get)来遍历集合元素,这样性能会更好;对于 LinkedList 集合,则应该采用迭代器(Iterator)来遍历集合元素;
- 如果需要经常执行插入、删除操作来改变包含大量数据的 List 集合的大小,可考虑使用 LinkedList 集合。使用 ArrayList、Vector 集合可能需要经常重新分配内部数组元素的大小,效果可能较差;
- 如果有多个线程需要同时访问 List 集合中的元素,开发者可考虑使用 Collections 将集合包装成线程安全的集合;