遍历Java中的列表的方法


575

对于Java语言有些陌生,我试图使自己熟悉所有可能遍历列表(或其他集合)的方式(或至少是非病理性方式)以及每种方式的优缺点。

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

基本的for 循环(当然,也有等效的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

    // ...
}

注意:正如@a​​marseillan指出的那样,这种形式对Lists 进行迭代是一个糟糕的选择,因为该get方法的实际实现可能不如使用时有效Iterator。例如,LinkedList实现必须遍历i之前的所有元素才能获得第i个元素。

在上面的示例中,List实现没有办法“保存其位置”以使将来的迭代更加有效。对于a而言,ArrayList这并不重要,因为的复杂度/成本get是恒定时间(O(1)),而对于a而言LinkedList,它的复杂度/成本与列表的大小(O(n))成正比。

有关内置Collections实现的计算复杂度的更多信息,请查看此问题

增强了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

    // ...
}

ListIterator

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.forEachStream.forEach,...

(来自Java 8的Stream API的map方法(请参阅@i_am_zero的答案)。)

在实现的Java 8集合类中Iterable(例如,all 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
这些都是非病理性的,尽管您也可以使用多种功能样式库中的任何一种来处理集合。
戴夫·牛顿

是关于List接口的问题吗?
Sotirios Delimanolis

@SotiriosDelimanolis,出于所有意图和目的,是的,它特定于<code> List </ code>,但是如果还有其他有趣的方式可以使用,例如<code> Collection </ code>,我会有兴趣认识他们。
iX3 2013年

@DaveNewton,谢谢你的主意。我从来没有用过这样的东西。请看一下我编辑过的问题,如果我理解您的意思,请告诉我。
iX3

Answers:


265

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

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

根据Java语言规范,其作用与显式使用带有传统循环的迭代器相同for。在第三种情况下,您只能通过删除当前元素来修改列表内容,然后再通过remove迭代器本身的方法进行操作即可。使用基于索引的迭代,您可以自由地以任何方式修改列表。但是,添加或删除当前索引之前的元素可能会导致循环跳过元素或多次处理同一元素;进行此类更改时,您需要适当地调整循环索引。

在所有情况下,element都是对实际列表元素的引用。没有一种迭代方法可以复制列表中的任何内容。内部状态的更改element将始终在列表中相应元素的内部状态中显示。

本质上,只有两种方法可以遍历列表:使用索引或使用迭代器。增强的for循环只是Java 5中引入的语法快捷方式,以避免显式定义迭代器的繁琐工作。对于这两种款式,你可以拿出本质琐碎的变化使用forwhiledo while块,但他们都归结为同样的事情(或者说,两件事情)。

编辑:正如@ iX3在评论中指出的那样,您可以ListIterator在迭代时使用a 来设置列表的当前元素。您需要使用List#listIterator()而不是List#iterator()初始化循环变量(显然,必须将其声明为ListIterator而不是Iterator)。


好的,谢谢,但是如果迭代器实际上正在返回对真实元素(不是副本)的引用,那么我如何使用它来更改列表中的值?我认为,如果我使用,e = iterator.next()那么所做的e = somethingElse只是更改对象e所指的内容,而不是更改从中iterator.next()检索值的实际存储。
iX3

@ iX3-基于索引的迭代也是如此;分配新对象e不会更改列表中的内容;你必须打电话list.set(index, thing)。您可以更改内容e(例如e.setSomething(newValue)),但什么元素存储在列表中你迭代它的变化,你需要坚持使用基于索引的迭代。
Ted Hopp

谢谢您的解释;我想我理解您现在在说什么,并将相应地更新我的问题/评论。本身没有“复制”功能,但是由于语言设计的原因,为了更改eI 的内容,我不得不调用e方法之一,因为赋值只是更改了指针(请原谅我的C语言)。
iX3

因此,对于像IntegerString这样的不可变类型的列表,将不可能使用for-eachIterator方法更改内容-必须操纵列表对象本身以替换为元素。那正确吗?
iX3

@ iX3-正确。您可以使用第三种形式(显式迭代器)删除元素,但是如果您使用基于索引的迭代,则只能向列表中的元素添加或替换元素。
Ted Hopp

46

问题中列出的每种示例:

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());
     }
}


20

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解决方案来更新顶部的列表。
iX3 2014年

1
@ eugene82这种方法更有效吗?
nazar_art 2014年

1
@nazar_art除非您在非常大的集合上使用parallelStream,否则您将看不到任何其他改进。实际上,仅在治愈冗长的意义上,它才更有效率。
Rogue

7

Java 8中,我们有多种方法来遍历集合类。

对每个使用Iterable

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

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

使用流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);

我们应该优先考虑forEachOrderedforEach因为的行为forEach是显式不确定的forEachOrdered,在这种情况下,如果流具有定义的遇到顺序,则在执行此流的每个元素的动作时,将按流的遇到顺序对其进行操作。因此,forEach不保证订单会得到保留。

流的优点在于,我们还可以在适当的地方使用并行流。如果目标是仅打印项目而不考虑顺序,则可以将并行流用作:

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

5

我不知道您认为病理是什么,但让我提供一些您之前从未见过的选择:

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方法。是的,我认为这是病态的,因为我不知道在任何情况下都可以使用这种方法(除了混淆竞赛以外)是有利的。
iX3

3
好。我不喜欢被称为“病理学”,所以这里有一个解释:我在跟踪或跟踪树木中的路径时使用了它们。多一个?在将某些Lisp程序转换为Java时,我不想让他们失去Lisp的精神,而是这样做了。如果您认为这些是有效的非病理用途,请发表评论。我需要一个团体拥抱!!!:-)
马里奥·罗西

树中的路径与列表不同吗?顺便说一句,我并不是说要让您或任何人“病态”,我只是说,我的问题的某些答案是不切实际的,或者在良好的软件工程实践中不具有价值。
iX3 2013年

@ iX3不用担心;我只是在开玩笑。路径为列表:“从根开始”(显而易见),“移至第二个孩子”,“移至第一个孩子”,“移至第四个孩子”。“停止”。或简称为[2,1,4]。这是一个清单。
马里奥·罗西

我希望创建列表的副本并使用while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }。这比第一个版本更有效;)。
AxelH


0

对于向后搜索,应使用以下内容:

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

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


0

是的,列出了许多替代方案。最简单,最干净的方法就是使用如下增强for语句。在Expression一些类型,它是可迭代的。

for ( FormalParameter : Expression ) Statement

例如,要遍历List <String> id,我们可以简单地做到这一点,

for (String str : ids) {
    // Do something
}

3
他正在问除了问题描述的方法(包括您提到的方法)之外,还有其他方法!
ericbn 2014年

0

在中java 8您可以使用List.forEach()方法与lambda expression遍历列表。

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的答案有何不同吗?
iX3

@ iX3啊。没有阅读整个答案列表。试图提供帮助。.您要我删除答案吗?
搜索结果网页结果'18 Pi

对我来说并不重要,尽管也许您可以通过将其与其他技术进行比较和对比来改进它。或者,如果您认为它没有展示任何新技术,但可能仍然对其他人有用,那么您可以添加注释以说明这一点。
iX3

-2

您总是可以使用while循环和更多的代码切换出第一个和第三个示例。这为您提供了可以使用do-while的优势:

int i = 0;
do{
 E element = list.get(i);
 i++;
}
while (i < list.size());

当然,如果list.size()返回0,则这种情况可能会导致NullPointerException,因为它总是至少执行一次。这可以通过在使用元素的属性/方法之前测试元素是否为空来解决。尽管如此,使用for循环要简单得多


是的...我猜这在技术上是不同的,但是仅当列表为空时,行为才不同,对吗?
iX3 2013年

@ ix3是的,在这种情况下,行为会更糟。我不会称其为好选择。
CPerkins 2013年

当然,但公平地说,这个问题不是关于什么是“好”,而是关于什么是“可能”,所以我仍然很欣赏这个答案。
iX3 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.