Java中遍历列表的方法

712

作为一个对Java语言有些陌生的人,我正在尝试熟悉所有遍历列表(或其他集合)的方法(至少是非病态的方法),以及每种方法的优缺点。

给定一个 List<E> list 对象,我知道以下遍历所有元素的方法:

基本 for loop(当然,也有等效的 while / do while 循环)

// Not recommended (see below)!
for (int i = 0; i < list.size(); i++) {
    E element = list.get(i);
    // 1 - can call methods of element
    // 2 - can use 'i' to make index-based calls to methods of list

    // ...
}

注意: 正如@amarseillan指出的那样,对于迭代List来说,这个表单是一个很糟糕的选择,因为get方法的实际实现可能不如使用Iterator高效。例如,LinkedList实现必须遍历i之前的所有元素才能获取第i个元素。
在上面的示例中,List实现无法"保存其位置"以使未来的迭代更加高效。对于ArrayList来说,这并不重要,因为get的复杂度/成本是常数时间(O(1)),而对于LinkedList来说,它与列表的大小成比例(O(n))。
有关内置Collections实现的计算复杂度的更多信息,请查看this question

增强型for循环(在这个问题中有很好的解释)

for (E element : list) {
    // 1 - can call methods of element

    // ...
}

迭代器

for (Iterator<E> iter = list.iterator(); iter.hasNext(); ) {
    E element = iter.next();
    // 1 - can call methods of element
    // 2 - can use iter.remove() to remove the current element from the list

    // ...
}

列表迭代器

for (ListIterator<E> iter = list.listIterator(); iter.hasNext(); ) {
    E element = iter.next();
    // 1 - can call methods of element
    // 2 - can use iter.remove() to remove the current element from the list
    // 3 - can use iter.add(...) to insert a new element into the list
    //     between element and iter->next()
    // 4 - can use iter.set(...) to replace the current element

    // ...
}

函数式Java

list.stream().map(e -> e + 1); // Can apply a transformation function for e

Iterable.forEach, Stream.forEach,...

(Java 8的Stream API中的map方法(请参见@i_am_zero的答案)。)

在Java 8中实现Iterable接口的集合类(例如所有的List)现在都有一个forEach方法,可以代替上面演示的for循环语句。(这里是另一个问题提供的一个很好的比较。)

Arrays.asList(1,2,3,4).forEach(System.out::println);
// 1 - can call methods of an element
// 2 - would need reference to containing object to remove an item
//     (TODO: someone please confirm / deny this)
// 3 - functionally separates iteration from the action
//     being performed with each item.

Arrays.asList(1,2,3,4).stream().forEach(System.out::println);
// Same capabilities as above plus potentially greater
// utilization of parallelism
// (caution: consequently, order of execution is not guaranteed,
// see [Stream.forEachOrdered][stream-foreach-ordered] for more
// information about this).

还有其他方式吗?如果有的话。

(顺便说一句,我的兴趣并不是源于想要优化性能;我只是想知道作为开发者可用的表单类型有哪些。)


1
这些是非病理性的,尽管您也可以使用几个函数式库来处理集合。 - Dave Newton
@SotiriosDelimanolis,就所有意图而言,是的,它是特定于<code>List</code>,但如果有其他有趣的方法可以处理,比如说一个<code>Collection</code>,我会很感兴趣了解它们。 - jacobq
@DaveNewton,谢谢你的建议。我从未使用过类似的东西。请看一下我编辑后的问题,让我知道我是否理解了你的意思。 - jacobq
我认为你应该将问题缩小,并将那些示例作为答案发布。 - sdasdadas
2
@sdasdadas,完成:https://dev59.com/b2Ml5IYBdhLWcg3wbGh1#18410379 - jacobq
显示剩余6条评论
13个回答

331

三种循环形式几乎相同。增强型for循环:

for (E element : list) {
    . . .
}

根据Java语言规范,使用增强型for循环与传统的for循环配合使用迭代器是完全等效的。在第三种情况下,您只能通过迭代器本身的remove方法删除当前元素来修改列表内容。使用基于索引的迭代,您可以自由地以任何方式修改列表。但是,添加或删除当前索引之前的元素会导致循环跳过元素或多次处理相同的元素;在进行此类更改时,需要适当调整循环索引。
在所有情况下,element都是实际列表元素的引用。迭代方法均不会复制列表中的任何内容。对element的内部状态进行更改将始终在列表上的相应元素的内部状态中看到。
基本上,遍历列表只有两种方法:使用索引或使用迭代器。增强型for循环只是Java 5中引入的一种语法快捷方式,用于避免明确定义迭代器的繁琐过程。对于这两种风格,你可以使用forwhiledo while块来得出基本上微不足道的变化,但它们都归结为同一件事情(或者说两件事情)。
编辑:正如@iX3在评论中指出的那样,你可以使用ListIterator在迭代时设置列表的当前元素。你需要使用List#listIterator()而不是List#iterator()来初始化循环变量(显然,循环变量必须声明为ListIterator而不是Iterator)。

好的,谢谢。但是如果迭代器实际上返回的是对真实元素的引用(而不是副本),那么我如何使用它来更改列表中的值呢?我认为如果我使用 e = iterator.next(),那么执行 e = somethingElse 只会更改 e 引用的对象,而不是更改 iterator.next() 检索到的实际存储。 - jacobq
@iX3 - 对于基于索引的迭代也是如此; 将一个新对象分配给e不会改变列表中的内容; 您必须调用list.set(index, thing)。 您可以更改e的内容(例如,e.setSomething(newValue)),但是要在迭代时更改存储在列表中的元素,您需要坚持使用基于索引的迭代。 - Ted Hopp
谢谢您的解释,我现在明白了您的意思,并会相应地更新我的问题/评论。并非完全“复制”,但由于语言设计,为了更改e的内容,我必须调用e的某个方法,因为赋值只会更改指针(请原谅我的C语言)。 - jacobq
那么对于像IntegerString这样的不可变类型的列表,使用for-eachIterator方法无法更改其内容--必须操作列表对象本身以替换元素。是这样吗? - jacobq
再次感谢。顺便说一下,似乎有一个特殊的ListIterator可以通过set进行替换。我也会将其添加到列表中。 - jacobq
显示剩余2条评论

54

问题中列出的每种类型的示例:

ListIterationExample.java

import java.util.*;

public class ListIterationExample {

     public static void main(String []args){
        List<Integer> numbers = new ArrayList<Integer>();

        // populates list with initial values
        for (Integer i : Arrays.asList(0,1,2,3,4,5,6,7))
            numbers.add(i);
        printList(numbers);         // 0,1,2,3,4,5,6,7

        // replaces each element with twice its value
        for (int index=0; index < numbers.size(); index++) {
            numbers.set(index, numbers.get(index)*2); 
        }
        printList(numbers);         // 0,2,4,6,8,10,12,14

        // does nothing because list is not being changed
        for (Integer number : numbers) {
            number++; // number = new Integer(number+1);
        }
        printList(numbers);         // 0,2,4,6,8,10,12,14  

        // same as above -- just different syntax
        for (Iterator<Integer> iter = numbers.iterator(); iter.hasNext(); ) {
            Integer number = iter.next();
            number++;
        }
        printList(numbers);         // 0,2,4,6,8,10,12,14

        // ListIterator<?> provides an "add" method to insert elements
        // between the current element and the cursor
        for (ListIterator<Integer> iter = numbers.listIterator(); iter.hasNext(); ) {
            Integer number = iter.next();
            iter.add(number+1);     // insert a number right before this
        }
        printList(numbers);         // 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

        // Iterator<?> provides a "remove" method to delete elements
        // between the current element and the cursor
        for (Iterator<Integer> iter = numbers.iterator(); iter.hasNext(); ) {
            Integer number = iter.next();
            if (number % 2 == 0)    // if number is even 
                iter.remove();      // remove it from the collection
        }
        printList(numbers);         // 1,3,5,7,9,11,13,15

        // ListIterator<?> provides a "set" method to replace elements
        for (ListIterator<Integer> iter = numbers.listIterator(); iter.hasNext(); ) {
            Integer number = iter.next();
            iter.set(number/2);     // divide each element by 2
        }
        printList(numbers);         // 0,1,2,3,4,5,6,7
     }

     public static void printList(List<Integer> numbers) {
        StringBuilder sb = new StringBuilder();
        for (Integer number : numbers) {
            sb.append(number);
            sb.append(",");
        }
        sb.deleteCharAt(sb.length()-1); // remove trailing comma
        System.out.println(sb.toString());
     }
}

27

基本的循环不建议使用,因为你不知道列表的实现方式。

如果这是一个链表,每次调用

list.get(i)

循环遍历列表,导致时间复杂度为 N^2。


1
正确;这是一个好观点,我会更新问题中的示例。 - jacobq
根据这篇文章,你的陈述是不正确的:哪个更有效率,for-each循环还是迭代器? - Hibbem
7
我在那篇文章中读到的与我说的完全一样...你到底读了哪一部分? - amarseillan

26
一个 JDK8 风格的迭代:
public class IterationDemo {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3);
        list.stream().forEach(elem -> System.out.println("element " + elem));
    }
}

谢谢,我打算最终用Java 8的解决方案更新列表顶部。 - jacobq
1
@eugene82 这种方法更有效吗? - catch23
1
@nazar_art 除非你在一个非常大的集合上使用 parallelStream,否则你不会看到比其他任何方法更好的改进。从实质上讲,它只是在减少冗长的代码方面更有效率而已。 - Rogue

9

在Java 8中,我们有多种方法来迭代集合类。

使用Iterable forEach

实现了Iterable接口的集合类(例如所有列表)现在都有forEach方法。我们可以使用Java 8中引入的方法引用

Arrays.asList(1,2,3,4).forEach(System.out::println);

使用Streams forEach和forEachOrdered

我们也可以使用Stream迭代一个列表,如下:

Arrays.asList(1,2,3,4).stream().forEach(System.out::println);
Arrays.asList(1,2,3,4).stream().forEachOrdered(System.out::println);

我们应该优先使用forEachOrdered而不是forEach,因为forEach的行为明确是不确定的,而forEachOrdered对于流中的每个元素执行一个操作,并按照流的遭遇顺序执行,如果流具有定义的遭遇顺序,则保证顺序。因此,forEach不能保证顺序被保留。
使用流的好处是,在适当的情况下,我们还可以利用并行流。如果目标仅是打印项目而不考虑顺序,则可以使用并行流:
Arrays.asList(1,2,3,4).parallelStream().forEach(System.out::println);

6
我不知道你认为什么是病态的,但是让我提供一些你可能之前没有看过的替代方案:
List<E> sl= list ;
while( ! sl.empty() ) {
    E element= sl.get(0) ;
    .....
    sl= sl.subList(1,sl.size());
}

或者它的递归版本:

void visit(List<E> list) {
    if( list.isEmpty() ) return;
    E element= list.get(0) ;
    ....
    visit(list.subList(1,list.size()));
}

此外,还有一种经典的for(int i=0...的递归版本:

void visit(List<E> list,int pos) {
    if( pos >= list.size() ) return;
    E element= list.get(pos) ;
    ....
    visit(list,pos+1);
}

我提到这些内容是因为你在Java方面还比较新,这可能会很有趣。

1
谢谢提醒。我从未看过subList方法。是的,我认为这是病态的,因为我不知道除了混淆比赛之外,在任何情况下使用它会有什么好处。 - jacobq
3
我不喜欢被称为“病态”,所以我来解释一下:我在跟踪或追踪树上的路径时使用它们。还有一个例子,当我将一些Lisp程序翻译成Java时,我不想让它们失去Lisp的精神,所以也这样做了。如果你认为这些是有效的、非病态的用途,请点赞。我需要一个团体拥抱! :-) - Mario Rossi
树中的路径不是与列表不同吗?顺便说一句,我并不是想称呼你或任何人为“病态”,我只是指出对于我的问题,有些答案在软件工程实践中可能不切实际或很少有价值。 - jacobq
@iX3 不用担心,我只是开玩笑。路径是列表:“从根开始”(显而易见),“移动到第二个子节点”,“移动到第一个子节点”,“移动到第四个子节点”。 “停止”。或简写为[2,1,4]。这是一个列表。 - Mario Rossi
我更喜欢创建列表的副本并使用 while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }。这比第一个版本更有效 ;)。 - AxelH

2

从Java 8开始,您可以使用forEach

 List<String> nameList   = new ArrayList<>(
            Arrays.asList("USA", "USSR", "UK"));

 nameList.forEach((v) -> System.out.println(v));

1
在Java 8中,您可以使用lambda表达式和List.forEach()方法来迭代列表。
import java.util.ArrayList;
import java.util.List;

public class TestA {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Apple");
        list.add("Orange");
        list.add("Banana");
        list.forEach(
                (name) -> {
                    System.out.println(name);
                }
        );
    }
}

好的。您能解释一下这与eugene82的答案i_am_zero的答案有何不同吗? - jacobq
@iX3 啊。没有读完整个答案列表。我试图提供帮助.. 你想让我删除答案吗? - Dil.
对我来说无所谓,但也许你可以通过与其他技术的比较和对比来改进它。或者如果你认为它没有展示任何新的技术,但仍然可能作为他人参考有用,也许你可以添加一条注释来解释。 - jacobq

1
在Java 8或以上的版本中,您可以使用forEach()方法迭代一个Hashset。
import java.util.HashSet;

public class HashSetTest {
    public static void main(String[] args) {
        
         HashSet<String> hSet = new HashSet<String>();
          
         // Adding elements into your HashSet usind add()
         hSet.add("test1");
         hSet.add("test2");
         hSet.add("test3");
      
         // Iterating over hash set items
         hSet.forEach(x -> System.out.println(x));
         // Or you can write shorter:
         hSet.forEach(System.out::println);
    }
}

你能详细说明一下这与问题中提到的 Iterable.forEach 有何不同吗?HashSet<T>Iterable<T> 的一个实现。(http://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html#forEach-java.util.function.Consumer-) - jacobq
据我所知,两者都用于遍历集合,但是如果有人需要“修改”集合,则应使用迭代器。forEach循环会抛出并发修改异常,因为它在后台不使用迭代器。 - Shila Mosammami
我想说的是,你的回答看起来像是 https://dev59.com/b2Ml5IYBdhLWcg3wbGh1#52205309 的重复。你有什么要补充的吗?那个问题中已经列出了“Iterable.forEach, Stream.forEach, ...”部分提到的内容。 - jacobq
我只是想尽可能地提供帮助。如果您认为不合适,我可以删除它。StackOverflow对我来说非常有益,符合我所追求的目标,我也愿意为他人尽可能多地做出贡献。无论如何,还是谢谢您。 - Shila Mosammami
1
个人认为这个答案并没有添加任何新内容(已经讨论过了),但我会留给其他人投票。非常感谢提供者的精神。 - jacobq

0

如果要进行反向搜索,您应该使用以下内容:

for (ListIterator<SomeClass> iterator = list.listIterator(list.size()); iterator.hasPrevious();) {
    SomeClass item = iterator.previous();
    ...
    item.remove(); // For instance.
}

如果您想知道一个位置,请使用iterator.previousIndex()。编写一个比较列表中两个位置的内部循环也会有所帮助(迭代器不相等)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接