在Java集合框架中,ListIterator是一个容易被忽视但极其重要的接口。作为Iterator的子接口,它专门为List类型集合设计,提供了比普通Iterator更强大的遍历和操作能力。我曾在多个项目中因为不了解ListIterator的特性而走了不少弯路,后来才发现它简直就是处理List集合的瑞士军刀。
ListIterator最显著的特点是支持双向遍历(向前和向后),并且可以在遍历过程中直接修改、添加元素。与普通Iterator只能单向移动且仅支持删除操作不同,ListIterator提供了更丰富的操作方法。在实际开发中,特别是需要频繁操作列表元素的场景,合理使用ListIterator可以大幅提升代码效率和可读性。
ListIterator最基础也最重要的特性就是双向遍历。通过hasNext()/next()和hasPrevious()/previous()两组方法的组合,我们可以自由地在列表中前后移动:
java复制List<String> list = Arrays.asList("A", "B", "C", "D");
ListIterator<String> it = list.listIterator();
while(it.hasNext()) {
System.out.print(it.next() + " "); // 输出:A B C D
}
while(it.hasPrevious()) {
System.out.print(it.previous() + " "); // 输出:D C B A
}
这种双向能力在处理需要回溯的场景时特别有用。比如在解析某些格式的数据时,可能需要向前查看之前的标记,这时ListIterator就派上用场了。
除了遍历,ListIterator还允许我们在迭代过程中直接修改当前元素或插入新元素:
java复制List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
ListIterator<String> it = list.listIterator();
it.next(); // 移动到"A"
it.set("A1"); // 修改"A"为"A1"
it.next(); // 移动到"B"
it.add("B1"); // 在"B"前插入"B1"
System.out.println(list); // 输出:[A1, B1, B, C]
这里有几个关键点需要注意:
ListIterator的游标位置概念需要特别注意。它不是指向某个元素,而是位于两个元素之间:
code复制元素1 元素2 元素3
^ ^ ^
游标0 游标1 游标2 游标3
next()返回游标右侧的元素并将游标右移,previous()返回游标左侧的元素并将游标左移。这种设计使得add()操作非常直观——总是在当前游标位置插入元素。
我们可以通过nextIndex()和previousIndex()方法获取游标位置:
java复制List<String> list = Arrays.asList("A", "B", "C");
ListIterator<String> it = list.listIterator();
System.out.println(it.nextIndex()); // 0
it.next();
System.out.println(it.nextIndex()); // 1
以ArrayList的ListIterator实现为例,它内部维护了几个关键状态:
ArrayList.ListItr的next()方法实现如下:
java复制public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
可以看到,每次next()都会:
ListIterator扩展了Iterator接口,主要增加了以下功能:
| 功能 | Iterator | ListIterator |
|---|---|---|
| 正向遍历 | ✓ | ✓ |
| 反向遍历 | ✗ | ✓ |
| 获取当前索引 | ✗ | ✓ |
| 修改当前元素 | ✗ | ✓ |
| 添加新元素 | ✗ | ✓ |
| 从指定位置开始迭代 | ✗ | ✓ |
从性能角度看,ListIterator的实现通常与Iterator相当,额外的功能几乎不会带来性能开销。但在某些特殊场景下(如LinkedList),反向遍历可能比正向遍历稍慢。
假设我们需要合并两个已排序的列表,并过滤掉重复元素:
java复制public static <T extends Comparable<T>> List<T> mergeAndDeduplicate(List<T> list1, List<T> list2) {
List<T> result = new ArrayList<>();
ListIterator<T> it1 = list1.listIterator();
ListIterator<T> it2 = list2.listIterator();
T item1 = it1.hasNext() ? it1.next() : null;
T item2 = it2.hasNext() ? it2.next() : null;
while (item1 != null || item2 != null) {
if (item1 == null) {
result.add(item2);
item2 = it2.hasNext() ? it2.next() : null;
} else if (item2 == null) {
result.add(item1);
item1 = it1.hasNext() ? it1.next() : null;
} else {
int cmp = item1.compareTo(item2);
if (cmp < 0) {
result.add(item1);
item1 = it1.hasNext() ? it1.next() : null;
} else if (cmp > 0) {
result.add(item2);
item2 = it2.hasNext() ? it2.next() : null;
} else {
result.add(item1);
item1 = it1.hasNext() ? it1.next() : null;
item2 = it2.hasNext() ? it2.next() : null;
}
}
}
return result;
}
这种方法比直接使用索引访问更安全,特别是在处理LinkedList时性能更好。
利用ListIterator的双向遍历能力,可以高效检测列表是否为回文:
java复制public static boolean isPalindrome(List<?> list) {
ListIterator<?> forward = list.listIterator();
ListIterator<?> backward = list.listIterator(list.size());
while (forward.hasNext() && backward.hasPrevious()) {
if (forward.nextIndex() >= backward.previousIndex()) {
break;
}
if (!Objects.equals(forward.next(), backward.previous())) {
return false;
}
}
return true;
}
这种方法只需要一次遍历即可完成检测,时间复杂度为O(n),且不需要额外的存储空间。
在文本处理中,我们经常需要批量替换列表中的元素:
java复制public static void replaceAll(List<String> list, String oldVal, String newVal) {
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
if (oldVal.equals(it.next())) {
it.set(newVal);
}
}
}
使用ListIterator的set()方法可以直接修改原列表,避免了创建新列表的开销。
ListIterator和Iterator一样,都实现了快速失败(fail-fast)机制。如果在迭代过程中列表被其他线程修改,或者通过列表自身的方法(而非迭代器的方法)修改了列表,就会抛出ConcurrentModificationException。
重要提示:要避免在迭代过程中通过原始列表引用修改列表结构(如add/remove),应该始终使用迭代器自身的方法进行修改。
对于ArrayList,ListIterator的性能与普通for循环接近。但对于LinkedList,ListIterator是最高效的遍历方式,因为:
选择合适的迭代方式:
注意游标位置:
异常处理:
线程安全:
在某些特殊场景下,我们可能需要实现自己的ListIterator。比如实现一个只读的ListIterator:
java复制public class UnmodifiableListIterator<E> implements ListIterator<E> {
private final ListIterator<E> delegate;
public UnmodifiableListIterator(ListIterator<E> delegate) {
this.delegate = delegate;
}
@Override
public boolean hasNext() { return delegate.hasNext(); }
@Override
public E next() { return delegate.next(); }
@Override
public boolean hasPrevious() { return delegate.hasPrevious(); }
@Override
public E previous() { return delegate.previous(); }
@Override
public int nextIndex() { return delegate.nextIndex(); }
@Override
public int previousIndex() { return delegate.previousIndex(); }
@Override
public void remove() { throw new UnsupportedOperationException(); }
@Override
public void set(E e) { throw new UnsupportedOperationException(); }
@Override
public void add(E e) { throw new UnsupportedOperationException(); }
}
这种模式在需要限制客户端操作时非常有用,比如在返回不可修改视图的集合时。
Java 8引入的Stream API提供了另一种处理集合的方式,但在某些场景下,ListIterator仍然不可替代:
java复制// 使用Stream实现元素替换
list = list.stream()
.map(s -> s.equals(oldVal) ? newVal : s)
.collect(Collectors.toList());
// 使用ListIterator实现原地替换
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
if (oldVal.equals(it.next())) {
it.set(newVal);
}
}
Stream方式更函数式,但会创建新列表;ListIterator方式可以原地修改,更节省内存。根据具体需求选择合适的方式。
ListIterator可以方便地实现内存中的分页处理:
java复制public static <T> List<T> getPage(List<T> source, int pageNumber, int pageSize) {
if (pageNumber < 1 || pageSize < 1) {
throw new IllegalArgumentException("页码和页大小必须大于0");
}
int fromIndex = (pageNumber - 1) * pageSize;
if (fromIndex >= source.size()) {
return Collections.emptyList();
}
int toIndex = Math.min(fromIndex + pageSize, source.size());
ListIterator<T> it = source.listIterator(fromIndex);
List<T> page = new ArrayList<>(pageSize);
while (it.hasNext() && it.nextIndex() < toIndex) {
page.add(it.next());
}
return page;
}
这种方法比subList更灵活,特别是在处理超大列表时,可以结合懒加载机制实现高效分页。