持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情
4 List集合
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引,例如第一次添加的元素索引为0,第二次添加的元素索引为1……
4.1 List接口和ListIterator接口
List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里增加了一些根据索引来操作集合元素的方法。
- void add(int index, Object element):将元素element插入到List集合的index处。
- 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(不包含)处所有集合元素组成的子集合。
所有的List实现类都可以调用这些方法来操作集合元素。**与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法。**下面程序示范了List集合的常规用法。
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));
}
}
上面程序中粗体字代码示范了List集合的独特用法,List集合可以根据位置索引来访问集合中的元素,因此List增加了一种新的遍历集合元素的方法:使用普通的for循环来遍历集合元素。运行上面程序,将看到如下运行结果:
[轻量级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关键字创建的新字符串对象,两个字符串显然不是同一个对象,但List的indexOf方法依然可以返回1。List判断两个对象相等的标准是什么呢?List判断两个对象相等只要通过equals()方法比较返回true即可。看下面程序。
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);
}
}
编译、运行上面程序,看到如下运行结果:
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义, 疯狂Android讲义]
[疯狂Android讲义]
从上面运行结果可以看出,执行①行代码时,程序试图删除一个A对象,List将会调用该A对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素——A类重写了equals()方法,该方法总是返回true。所以我们看到每次从List集合中删除A对象,总是删除List集合中的第一个元素。
注意:
当调用List的set(int index, Object element)方法来改变List集合指定索引处的元素时,指定的索引必须是List集合的有效索引。例如集合长度是4,就不能指定替换索引为4处的元素——也就是说,set(int index, Object element)方法不会改变List集合的长度。
与Set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。ListIterator接口在Iterator接口基础上增加了如下方法。
- boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
- Object previous():返回该迭代器的上一个元素。
- void add():在指定位置插入一个元素。
拿ListIterator与普通的Iterator进行对比,不难发现ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator还可通过add方法向List集合中添加元素(Iterator只能删除元素)。下面程序示范了ListIterator的用法。
public class ListIteratorTest
{
public static void main(String[] args)
{
String[] books={
"疯狂Java讲义",
"轻量级Java EE企业应用实战"
};
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()方法向上一次迭代元素的后面添加一个新元素。
4.2 ArrayList和Vector实现类
ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能
ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出了该数组的长度时,它们的initialCapacity会自动增加。
对于通常的编程场景,程序员无须关心ArrayList或Vector的initialCapacity。但如果向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。
如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。如果创建空的ArrayList或Vector集合时不指定initialCapacity参数,则Object[]数组的长度默认为10。
除此之外,ArrayList和Vector还提供了如下两个方法来重新分配Object[]数组。
- void
ensureCapacity(int minCapacity):将ArrayList或Vector集合的Object[]数组长度增加minCapacity。 - void
trimToSize():调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。程序可调用该方法来减少ArrayList或Vector集合对象占用的存储空间。
提示: Vector里有一些功能重复的方法,这些方法中方法名更短的方法属于后来新增的方法,方法名更长的方法则是Vector原有的方法。Java改写了Vector原有的方法,将其方法名缩短是为了简化编程。而ArrayList开始就作为List的主要实现类,因此没有那些方法名很长的方法。实际上,Vector具有很多缺点,通常尽量少用Vector实现类。
除此之外,ArrayList和Vector的显著区别是:ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但Vector集合则是线程安全的,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也同样不推荐使用Vector实现类。后面会介绍一个Collections工具类,它可以将一个ArrayList变成线程安全的。
Vector还提供了一个Stack子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。与Java中的其他集合一样,进栈出栈的都是Object,因此从栈中取出元素后必须进行类型转换,除非你只是使用Object具有的操作。所以Stack类里提供了如下几个方法。
-
Object
peek():返回“栈”的第一个元素,但并不将该元素“pop”出栈。 -
Object
pop():返回“栈”的第一个元素,并将该元素“pop”出栈。 -
void
push(Object item):将一个元素“push”进栈,最后一个进“栈”的元素总是位于“栈”顶。
public class VectorTest
{
public static void main(String[] args)
{
Stack v=new Stack();
//依次将三个元素“push”入栈
v.push("疯狂Java讲义");
v.push("轻量级Java EE企业应用实战");
v.push("疯狂Android讲义");
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(v);
//访问第一个元素,但并不将其“pop”出栈,输出:疯狂Android讲义
System.out.println(v.peek());
//依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(v);
//“pop”出栈第一个元素,输出:疯狂Android讲义
System.out.println(v.pop());
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(v);
}
}
需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它是线程安全的,性能比较差,因此现在的程序中一般较少使用Stack类。如果程序需要使用“栈”这种数据结构,则可以考虑使用LinkedList。
LinkedList也是List的实现类,它是一个基于链表实现的List类,对于顺序访问集合中的元素进行了优化,特别是插入、删除元素时速度非常快。LinkedList既实现了List接口,也实现了Deque接口,由于实现了Deque接口,因此可以作为栈来使用。
4.3 固定长度的List
前面讲数组时介绍了一个操作数组的工具类:Arrays,该工具类里提供了asList(Object... a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。如下程序所示。
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的集合元素
for (int i=0; i < fixedList.size() ; i++)
{
System.out.println(fixedList.get(i));
}
//试图增加、删除元素都会引发UnsupportedOperationException异常
fixedList.add("疯狂Android讲义");
fixedList.remove("疯狂Java讲义");
}
}
5 Queue集合
Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
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两个实现类。
5.1 PriorityQueue实现类
PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。
PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,PriorityQueue的元素有两种排序方式。
- 自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
- 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。
5.2 Deque接口与ArrayDeque实现类
Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。
-
void addFirst(Object e):将指定元素插入该双端队列的开头。
-
void addLast(Object e):将指定元素插入该双端队列的末尾。
-
Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素
-
Object getFirst():获取但不删除双端队列的第一个元素。
-
Object getLast():获取但不删除双端队列的最后一个元素。
-
boolean offerFirst(Object e):将指定元素插入该双端队列的开头。
-
boolean offerLast(Object e):将指定元素插入该双端队列的末尾。
-
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。
-
removeLast():获取并删除该双端队列的最后一个元素。
-
removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素o。
从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。
Deque的方法与Queue的方法对照表
Deque的方法与Stack的方法对照表
Deque接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
**提示:**ArrayList和ArrayDeque两个集合类的实现机制基本相似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素。
下面程序示范了把ArrayDeque当成“栈”来使用。
public class ArrayDequeTest
{
public static void main(String[] args)
{
ArrayDeque stack=new ArrayDeque();
//依次将三个元素push入“栈”
stack.push("疯狂Java讲义");
stack.push("轻量级Java EE企业应用实战");
stack.push("疯狂Android讲义");
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(stack);
//访问第一个元素,但并不将其pop出“栈”,输出:疯狂Android讲义
System.out.println(stack.peek());
//依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(stack);
//pop出第一个元素,输出:疯狂Android讲义
System.out.println(stack.pop());
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(stack);
}
}
上面程序的运行结果与前面使用Stack的运行结果相似,不过使用ArrayDeque的性能会更加出色,因此现在的程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque或LinkedList,而不是Stack。
5.3 LinkedList实现类
LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,因此它可以被当成双端队列来使用,自然也可以被当成“栈”来使用了。下面程序简单示范了LinkedList集合的用法。
public class LinkedListTest
{
public static void main(String[] args)
{
LinkedList books=new LinkedList();
//将字符串元素加入队列的尾部
books.offer("疯狂Java讲义");
//将一个字符串元素加入栈的顶部
books.push("轻量级Java EE企业应用实战");
//将字符串元素添加到队列的头部(相当于栈的顶部)
books.offerFirst("疯狂Android讲义");
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与ArrayList、ArrayDeque的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能非常出色(只需改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能,所以各方面性能都有所下降。
注意: 对于所有的内部基于数组的集合实现,例如ArrayList、ArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。
5.4 各种线性表的性能分析
public class PerformanceTest
{
public static void main(String[] args)
{
//创建一个字符串数组
String[] tst1=new String[900000];
//动态初始化数组元素
for (int i=0; i < 900000; i++)
{
tst1[i]=String.valueOf(i);
}
ArrayList al=new ArrayList();
//将所有的数组元素加入ArrayList集合中
for (int i=0; i < 900000 ; i++)
{
al.add(tst1[i]);
}
LinkedList ll=new LinkedList();
//将所有的数组元素加入LinkedList集合中
for (int i=0; i < 900000 ; i++)
{
ll.add(tst1[i]);
}
//迭代访问ArrayList集合的所有元素,并输出迭代时间
long start=System.currentTimeMillis();
for (Iterator it=al.iterator();it.hasNext() ; )
{
it.next();
}
System.out.println("迭代ArrayList集合元素的时间:"
+ (System.currentTimeMillis() - start));
//迭代访问LinkedList集合的所有元素,并输出迭代时间
start=System.currentTimeMillis();
for (Iterator it=ll.iterator();it.hasNext() ; )
{
it.next();
}
System.out.println("迭代LinedList集合元素的时间:"
+ (System.currentTimeMillis() - start));
}
}
由于上面程序创建了一个长度为900000的字符串数组,需要很大的内存空间,JVM默认的内存空间不足以运行上面程序,所以应该采用如下命令来运行上面程序:
java -Xms128m -Xmx512m PerformanceTest
- -Xms是设置JVM的堆内存初始大小。
- -Xmx是设置JVM的堆内存最大大小(最好不要超过物理内存大小)。
多次运行上面程序会发现,迭代ArrayList集合的时间略大于迭代LinkedList集合的时间。因此,关于使用List集合有如下建议。
- 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素。
- 如果需要经常执行插入、删除操作来改变List集合的大小,则应该使用LinkedList集合,而不是ArrayList。使用ArrayList、Vector集合需要经常重新分配内部数组的大小,其时间开销常常是使用LinkedList的时间开销的几十倍,效果很差。
- 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。