Способы перебора списка в Java

576

Будучи немного новым для языка Java, я пытаюсь ознакомиться со всеми способами (или, по крайней мере, с непатологическими), которые можно перебрать в списке (или, возможно, с другими коллекциями), а также с преимуществами или недостатками каждого из них.

Для данного List<E> listобъекта я знаю следующие способы прохождения всех элементов:

Базовый для цикла (конечно, есть и эквивалентные 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, эта форма является плохим выбором для итерации по Lists, поскольку фактическая реализация getметода может быть не такой эффективной, как при использовании Iterator. Например, LinkedListреализации должны пройти все элементы, предшествующие i, чтобы получить i-й элемент.

В приведенном выше примере Listреализация не может "сохранить свое место", чтобы сделать будущие итерации более эффективными. Для a ArrayListэто на самом деле не имеет значения, потому что сложность / стоимость getявляется постоянным временем (O (1)), тогда как для a LinkedListэто пропорционально размеру списка (O (n)).

Для получения дополнительной информации о вычислительной сложности встроенных Collectionsреализаций, проверьте этот вопрос .

Улучшено для цикла (хорошо объяснено в этом вопросе )

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

(Метод карты из Stream API Java 8 (см. Ответ @ 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).

Какие еще есть способы, если таковые имеются?

(Кстати, мой интерес совсем не связан с желанием оптимизировать производительность ; я просто хочу знать, какие формы доступны мне как разработчику.)

ix3
источник
1
Это непатологические, хотя вы также можете использовать любую из нескольких библиотек функционального стиля для обработки коллекций.
Дэйв Ньютон
Это вопрос об Listинтерфейсе конкретно?
Сотириос Делиманолис
@SotiriosDelimanolis, для всех намерений и целей, да, он специфичен для <code> List </ code>, но если есть другие интересные способы работы, скажем, с <code> Collection </ code>, я бы интересно узнать их.
iX3
@DaveNewton, спасибо за идею. Я никогда не использовал ничего подобного. Пожалуйста, посмотрите на мой отредактированный вопрос и дайте мне знать, понял ли я, что вы имели в виду.
iX3
2
@sdasdadas, сделано: stackoverflow.com/questions/18410035/…
iX3

Ответы:

265

Три формы зацикливания почти идентичны. Усовершенствованный forцикл:

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

является, согласно спецификации языка Java , идентичного по эффекту явного использования итератора с традиционным forконтуром. В третьем случае вы можете изменить содержимое списка, только удалив текущий элемент, и только тогда, если вы сделаете это через removeметод самого итератора. С итерацией на основе индекса вы можете изменять список любым способом. Однако добавление или удаление элементов, предшествующих текущему индексу, может привести к пропуску элементов цикла или обработке одного и того же элемента несколько раз; вам нужно правильно настроить индекс цикла при внесении таких изменений.

Во всех случаях elementэто ссылка на фактический элемент списка. Ни один из методов итерации не создает копию чего-либо в списке. Изменения во внутреннем состоянии elementвсегда будут видны во внутреннем состоянии соответствующего элемента в списке.

По сути, существует только два способа перебора списка: с помощью индекса или с помощью итератора. Усовершенствованный цикл for - это всего лишь синтаксический ярлык, введенный в Java 5, чтобы избежать скуки явного определения итератора. Для обоих стилей вы можете придумать по существу тривиальные варианты, используя for, whileили do whileблоки, но все они сводятся к одному и тому же (или, скорее, двум вещам).

РЕДАКТИРОВАТЬ: Как @ iX3 указывает в комментарии, вы можете использовать a, ListIteratorчтобы установить текущий элемент списка во время итерации. Вы должны будете использовать 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)), но для изменения того, что элемент хранится в списке , как вы итерацию, вы должны придерживаться индекса на основе итерации.
Тед Хопп
Спасибо за это объяснение; Я думаю, что я понимаю, что вы говорите сейчас, и обновлю мой вопрос / комментарии соответственно. Там нет «копирования» как такового, но из-за языкового дизайна, чтобы внести изменения в содержимое, eмне пришлось бы вызывать один из eметодов, потому что присваивание просто меняет указатель (простите мой C).
iX3
Таким образом, для списков неизменяемых типов, таких как Integerи, Stringбыло бы невозможно изменить содержимое с помощью методов for-eachили Iterator- пришлось бы манипулировать самим объектом списка для замены на элементы. Это правильно?
iX3
@ iX3 - правильно. Вы можете удалить элементы с помощью третьей формы (явный итератор), но вы можете добавлять или заменять элементы только в списке, если вы используете итерацию на основе индекса.
Тед Хопп
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());
     }
}
ix3
источник
23

Базовый цикл не рекомендуется, так как вы не знаете реализацию списка.

Если это был LinkedList, каждый вызов

list.get(i)

будет перебирать список, что приведет к сложности времени N ^ 2.

amarseillan
источник
1
Правильный; это хороший момент, и я обновлю пример в вопросе.
iX3
согласно этому посту, ваше утверждение неверно: что является более эффективным: цикл для каждого или итератор?
Хиббем
5
То, что я прочитал в этом посте, именно то, что я сказал ... Какую часть вы читаете именно?
Amarseillan
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));
    }
}
eugene82
источник
Спасибо, я намерен обновить список в верхней части с решениями Java 8 в конце концов.
iX3
1
@ eugene82 этот подход намного эффективнее?
nazar_art
1
@nazar_art Вы не увидите значительных улучшений по сравнению с чем-либо еще, если, возможно, вы не используете parallelStream в очень большой коллекции. На самом деле это более эффективно только в смысле исцеляющего многословия.
Изгой
7

В Java 8 у нас есть несколько способов перебора классов коллекции.

Использование Iterable forEach

Коллекции, которые реализуют 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);

Мы должны отдавать предпочтение forEachOrderedболее forEachпотому , что поведение forEachявно недетерминировано , где , как и forEachOrderedвыполняет действие для каждого элемента этого потока, в порядке столкновения потока , если поток имеет определенный порядок встречи. Таким образом, forEach не гарантирует, что заказ будет сохранен.

Преимущество потоков заключается в том, что мы также можем использовать параллельные потоки там, где это необходимо. Если цель состоит только в том, чтобы печатать элементы независимо от порядка, то мы можем использовать параллельный поток как:

Arrays.asList(1,2,3,4).parallelStream().forEach(System.out::println);
akhil_mittal
источник
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
ХОРОШО. Мне не нравится, когда меня называют «патологическим», поэтому здесь приводится одно объяснение: я использовал их при отслеживании или следовании по дорожкам на деревьях. Еще? При переводе некоторых программ на Лисп на Java я не хотел, чтобы они теряли дух Лиспа, и сделал то же самое. Добавьте комментарий, если считаете, что это допустимое, непатологическое использование. Мне нужно групповое объятие !!! :-)
Марио Росси
Разве пути в деревьях не отличаются от списков? Кстати, я не хотел называть вас или любого другого человека "патологическим", я просто имею в виду, что некоторые ответы на мой вопрос будут по понятным причинам неосуществимыми или очень маловероятными, чтобы иметь значение в хорошей практике разработки программного обеспечения.
iX3
@ iX3 Не волнуйся; Я просто пошутил. Пути - это списки: «начать с корня» (очевидно), «перейти к 2-му дочернему элементу», «перейти к 1-му дочернему элементу», «перейти к 4-му дочернему элементу». "Стоп". Или [2,1,4] для краткости. Это список.
Марио Росси
Я бы предпочел создать копию списка и использовать while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }. Это более эффективно, чем первая версия;).
AxelH
2

Вы можете использовать forEach начиная с Java 8:

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

 nameList.forEach((v) -> System.out.println(v));
Судип Бхандари
источник
0

Для обратного поиска вы должны использовать следующее:

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

Если вы хотите узнать позицию, используйте iterator.previousIndex (). Это также помогает написать внутренний цикл, который сравнивает две позиции в списке (итераторы не равны).

CoolMind
источник
0

Да, многие альтернативы перечислены. Самым простым и понятным было бы просто использовать расширенное forутверждение, как показано ниже. Это Expressionнекоторый тип, который является итеративным.

for ( FormalParameter : Expression ) Statement

Например, чтобы перебрать идентификаторы List <String>, мы можем просто так,

for (String str : ids) {
    // Do something
}
Ю Чен
источник
3
Он спрашивает, есть ли другие способы, кроме описанных в вопросе (включая этот, который вы упомянули)!
ericbn
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);
                }
        );
    }
}
Результаты поиска Веб-результаты Pi
источник
Круто. Не могли бы вы объяснить, чем это отличается от eugene82ответа и i_am_zeroответа ?
iX3
@ iX3 ааа. Не прочитал весь список ответов. Пытался быть полезным .. Вы хотите, чтобы я удалил ответ?
Результаты поиска веб-результаты Pi
Не имеет значения для меня, хотя, возможно, вы могли бы улучшить его, сравнивая и противопоставляя его другим методам. Или, если вы думаете, что он не демонстрирует никакой новой техники, но все же может быть полезен в качестве справочного материала для других, возможно, вы могли бы добавить примечание, объясняющее это.
iX3
-2

Вы всегда можете отключить первый и третий примеры с помощью цикла while и немного большего количества кода. Это дает вам возможность использовать do-while:

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

Конечно, такого рода вещи могут вызвать исключение NullPointerException, если list.size () возвращает 0, потому что он всегда выполняется хотя бы один раз. Это можно исправить, проверив, является ли элемент нулевым, прежде чем использовать его атрибуты / методы tho. Тем не менее, это намного проще и проще использовать цикл for

shieldgenerator7
источник
Верно ... Я думаю, это технически отличается, но поведение отличается, только если список пуст, верно?
iX3
@ ix3 да, и в этом случае поведение хуже. Я бы не назвал это хорошей альтернативой.
CPerkins
Конечно, но, честно говоря, этот вопрос не столько о том, что «хорошо», сколько о том, что «возможно», поэтому я все еще ценю этот ответ.
iX3