List接口
一个List是一个有序的集合(也可以叫作序列)。List可以包含重复元素。除去从Collection继承下来的操作之外, List接口还有以下操作:
- 位置访问 - 根据元素在List中的数字下标来访问它们。方法有:
get, set, add, addAll,
和remove
。 - 查找 - 在List查找指定元素, 并且返回数字下标。方法有
indexOf
和lastIndexOf
。 - 迭代 - 继承迭代的语义并且利用到List的连续性。方法有:
listIterator
- 区间视图 -
sublist
方法可以对List进行任意的区间操作。
Java包含两个List接口的通用实现。 ArrayList在实现上有更好的性能, LinkedList在一些特定的场合有更好的性能。
集合操作
假如你已经熟悉了集合操作, 那么从集合继承下来的操作,在List这里和之前你看的集合操作是一样的,没有什么不同。 如果你还不熟悉集合操作,现在就去看。remove
操作会移去在List中第一次出现的元素 (因为List允许重复元素)。add
和addAll
操作总是把新的元素加在List的末尾,因此下面的语句就是把一个List和另外一个连接起来:
list1.addAll(list2);
下面是一个不破坏原有结构的操作,新创建第三个list,把第一个和第二个连接起来:
List<Type> list3 = new ArrayList<Type>(list1);
list3.addAll(list2);
注意一下上面的语句,它还用到了ArrayList
标准的转换式构造方法。
下面的例子(JDK 8或者更高级版本)是用聚合方法把名字放到一个list中去:
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
和Set一样,List也重新定义了equals
和hashCode
方法,这样就能逻辑上比较两个List而不用关心他们的实现类。如果两个List包含的元素相同,顺序也相同,它们就是相等的。
位置访问和查找操作
基本的位置访问操作包括:get
,set
, add
和remove
(set
和remove
操作会返回将要被覆盖或者删除的元素)。其它的操作(indexOf
和lastIndexOf
)返回指定元素第一次或者最后一次出现的位置。
addAll
操作可以把一个集合的所有元素插入到list的指定位置处。元素的插入顺序是和集合迭代返回的顺序一致的。该操作实际上是模拟Collection的addAll。
下面的方法是用来交换List的两个元素(已经知道它们的索引)
public static <E> void swap(List<E> a, int i, int j) {
E tmp = a.get(i);
a.set(i, a.get(j));
a.set(j, tmp);
}
这是一个多态的算法,它可以交换任意List的两个元素,不管这个List的实现类是什么样的。下面还有一个多态的例子,它用到了我们刚刚看到的swap:
public static void shuffle(List<?> list, Random rnd) {
for (int i = list.size(); i > 1; i--)
swap(list, i - 1, rnd.nextInt(i));
}
这个方法,在Java Collection的类中就有,它根据你指定的随机函数把list里面的元素顺序随机一下。有一点微妙之处:算法是从List的末尾元素开始,重复的把随机位置的元素和当前元素交换。不像其它原生的改变顺序,这个方法平稳(假定提供的随机函数很好,所有的元素都几乎会被改序)而快速(刚好需要长度减1次交换)。下面的程序会用到这个方法, 它把运行参数的所有单词随机输出出来:
import java.util.*;
public class Shuffle {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (String a : args)
list.add(a);
Collections.shuffle(list, new Random());
System.out.println(list);
}
}
实际上上面的程序可以修改成代码更少性能更好的版本。Arrays
类有一个静态工厂方法叫做asList
,可以把数组当成list来查看。此方法不会复制数组, 因此list上的改动也会改动数组,反之亦然(改动数组也会改动list)。并且此方法返回的list不是一个正常的list,你不能对这个返回的list进行add
和remove
操作, 因为数组本身就是长度不可变的。我们利用Arrays.asList
的优势, 再用默认的随机函数调用一下shuffle
方法,程序就可以修改下面的精简版:
import java.util.*;
public class Shuffle {
public static void main(String[] args) {
List<String> list = Arrays.asList(args);
Collections.shuffle(list);
System.out.println(list);
}
}
迭代
正如你所料,List's iterator
方法返回的迭代器包含了List的所有元素的正确序列。List还提供了一个更加丰富的迭代器,那就是ListIterator
, 它可以让你从list的不同方向来遍历,可以在迭代的过程中修改list,还可以获取当前迭代的位置。
ListIterator
从Iterator
继承的三个方法hasNext, next,
和remove
, 在功能上没有什么变动,和原来的一样。 ListIterator
还有hasPrevious
和previous
方法,这两个方法就是完全模拟hasNext
和 next
。前者是指向当前游标(隐式的)的前一个元素,而后者是指向当前游标的后一个元素。 previous
是把当前游标后退, 而next
是把当前游标前进。
下面是标准的反向迭代一个list的操作:
for (ListIterator<Type> it = list.listIterator(list.size()); it.hasPrevious(); ) {
Type t = it.previous();
...
}
注意这个例子中listIterator
方法的参数。在list接口中,方法listIterator
有两种形式,一种是无参数的,会返回一个ListIterator
游标指向list的开始位置;有一种是有一个整型的参数,返回一个ListIterator
游标指向list的整型参数位置, 通过调用方法next
, 会返回游标位置所对应的元素;通过调用方法previous
,会返回游标位置减1所对应的元素。如果一个list长度是n,那么就有n+1个有效的游标位置:0到n.
更直观的讲,游标总是处在两个元素之间:调用previous
返回的那个元素和调用next
返回的那个元素之间。n+1个有效的游标位置就对应着n个元素间的空隙,从第一个元素之前到最后一个元素之后。下图就展示了一个含有4个元素list的5个有效游标位置:
虽然next
和previous
方法可以混合调用,但是你最好仔细点。第一次调用previous
和最后一次调用next
返回的元素是同一个。同样的,第一次调用next
和最后一次调用previous
返回的元素也是同一个。
以下的方法,你也就不会觉得稀奇了。nextIndex
方法会返回接下来调用next
所返回的元素的位置;previousIndex
方法会返回接下调用previous
所返回的元素的位置。这种调用经常会用在:1.找到了特定的东西,需要返回当前位置 2. 记录当前ListIterator
位置,以便创建另外一个拥有相同位置的ListIterator
。
你应该也不会惊讶,nextIndex
返回的值总是比previousIndex
大1。这暗示了这种行为有两个边界情况:1.当游标在第一个元素之前的时候, previousIndex
返回-1 2.当游标在最后一个元素之后的时候,nextIndex
就返回list.size()
. 为了使这些更具体详细一些, 下面是List.indexOf
的一种可能的实现方式:
public int indexOf(E e) {
for (ListIterator<E> it = listIterator(); it.hasNext(); )
if (e == null ? it.next() == null : e.equals(it.next()))
return it.previousIndex();
// Element not found
return -1;
}
注意,方法indexOf
返回的是it.previousIndex()
, 虽然我们是从前向后遍历的list. 这是因为,it.nextIndex()
会返回我们将要处理的元素的位置, 而it.previousIndex()
会返回 我们刚刚处理过的元素位置。ListIterator
还提供了两个用于修改list的方法-set
和add
。 set
方法会把最后调用next
或者previous
方法返回的元素修改掉。下面的多态方法,会把所有出现的指定的元素替换成另外一个:
public static <E> void replace(List<E> list, E val, E newVal) {
for (ListIterator<E> it = list.listIterator(); it.hasNext(); )
if (val == null ? it.next() == null : val.equals(it.next()))
it.set(newVal);
}
这个例子唯一特殊的地方就是处理val
和it.next()
之间的比较, 需要特殊处理null
,不然会报空指针异常。
add
方法会紧贴着游标前面插入一个新元素,下面的多形方法就实现了功能:把一个list中指定的元素用另外一个list中的一串元素替换掉:
public static <E>
void replace(List<E> list, E val, List<? extends E> newVals) {
for (ListIterator<E> it = list.listIterator(); it.hasNext(); ){
if (val == null ? it.next() == null : val.equals(it.next())) {
it.remove();
for (E e : newVals)
it.add(e);
}
}
}
区间视图
区间视图或者局部视图的操作是subList(int fromIndex, int toIndex)
, 它会返回list的一部分, 这部分从fromIndex
开始(包括这个位置)到toIndex
结束(不包括这个位置), 这种半开区间(因为只包含fromIndex的元素,不包含结束的)和下面的for循环效果一样的:
for (int i = fromIndex; i < toIndex; i++) {
...
}
就如同视图的含义一样,返回的区间是原list的一部分,没有复制出来, 也就说修改原来的list, 相应的视图也会变化。
有了这个方法, list上的很多区间操作能够直接借用它而不需要额外指明区间再来操作。例如下面的代码就是移除list上的一个区间段:
list.subList(fromIndex, toIndex).clear();
下面的代码是只在指定的区间内查找某个元素:
int i = list.subList(fromIndex, toIndex).indexOf(o);
int j = list.subList(fromIndex, toIndex).lastIndexOf(o);
请注意,上面这段代码返回的下标位置是元素在subList的中的位置, 不是整个List的位置。
任何能在List上进行多态运算如replace, shuffle
,都能在subList进行。
下面是一个利用sublist的多态算法, 它实现了如何从一幅牌里发一手出去。具体点说, 该方法会返回一个新的list(一手牌),这个新的list是从另外一个List(整幅牌)的尾部取出来的包含一定数量元素。 所有在手牌里的元素会从整幅牌里除去(拿掉就存在原来的牌堆中了)
public static <E> List<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
List<E> hand = new ArrayList<E>(handView);
handView.clear();
return hand;
}
请注意,这手幅是从整幅牌的尾部拿的。对于大部分的List的实现类,如ArrayList,从尾部移除元素要比从头部移除性能上要好一些。
下面的程序结合方法dealHand
和Collections.shuffle
能够从一幅52张的牌里随机生成几手牌。程序有两个运行参数:(1)生成几手 (2)每一手的牌数
import java.util.*;
public class Deal {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Usage: Deal hands cards");
return;
}
int numHands = Integer.parseInt(args[0]);
int cardsPerHand = Integer.parseInt(args[1]);
// Make a normal 52-card deck.
String[] suit = new String[] {
"spades", "hearts",
"diamonds", "clubs"
};
String[] rank = new String[] {
"ace", "2", "3", "4",
"5", "6", "7", "8", "9", "10",
"jack", "queen", "king"
};
List<String> deck = new ArrayList<String>();
for (int i = 0; i < suit.length; i++)
for (int j = 0; j < rank.length; j++)
deck.add(rank[j] + " of " + suit[i]);
// Shuffle the deck.
Collections.shuffle(deck);
if (numHands * cardsPerHand > deck.size()) {
System.out.println("Not enough cards.");
return;
}
for (int i = 0; i < numHands; i++)
System.out.println(dealHand(deck, cardsPerHand));
}
public static <E> List<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
List<E> hand = new ArrayList<E>(handView);
handView.clear();
return hand;
}
}
运行程序如下:
% java Deal 4 5
[8 of hearts, jack of spades, 3 of spades, 4 of spades,
king of diamonds]
[4 of diamonds, ace of clubs, 6 of clubs, jack of hearts,
queen of hearts]
[7 of spades, 5 of spades, 2 of diamonds, queen of diamonds,
9 of clubs]
[8 of spades, 6 of diamonds, ace of spades, 3 of hearts,
ace of hearts]
尽管subList操作非常强大,但是在使用的时候还是有一些要注意的。当你对返回了subList之后 , 对原有的List进行添加、删除操作而不是在subList上操作, subList在语义上就不成立了(开始结束位置就不和最初设想的一样了)。因此强烈建议你只把sublist当成一个瞬时的对象来用,也就是执行原有List的一区间操作时用到它。你使用保留subList实例的时间越长,就越有可能原有List被修改了(或者被其它sublist修改了)。请注意,修改一个subList的subList是合法的并且你还可以继续使用原来subList(虽然不是同步的)。
List的算法
大部分Collections
的多态算法也适用于List
。合理使用这些算法可以非常容易的操纵List
, 下面是对这些地算法的总结 ,如果你想要看更多细节,请参照算法章节
- sort — 用合并排序算法对List排序,是一种快速稳定的算法(稳定的排序算法就是相等的元素不会被打乱)
- shuffle — 随机打乱List里的元素
- reverse — 对List的元素反向排序
- rotate — 根据指定的距离对List元素翻转
- swap — 把List指定位置的元素交换
- replaceAll — 把所有出现过的某个元素替换成另外一个
- fill — 把List的每一个元素用另外一个替换
- copy — 把一个List的元素复制到另外一个List中去
- binarySearch — 在一个排序好的List中用二分法查找指定元素
- indexOfSubList — 查找字串第一次出现的位置
- indexOfSubList — 查找字串最后一次出现的位置
http://docs.oracle.com/javase/tutorial/collections/interfaces/list.html