Способи перегляду списку на 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, ця форма є поганим вибором для повторення часу List, оскільки реальна реалізація getметоду може бути не такою ефективною, як при використанні Iterator. Наприклад, LinkedListреалізації повинні пройти всі елементи, що передують i, щоб отримати i-й елемент.

У наведеному вище прикладі немає способу Listреалізації "зберегти своє місце", щоб зробити майбутні ітерації більш ефективними. Для цього 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 , ...

(Метод карти з потокового API Java 8 (див. Відповідь @ i_am_zero).)

У класах колекції Java 8, які реалізують Iterable(наприклад, всі Lists), тепер є forEachметод, який можна використовувати замість викладеного вище оператора циклу . (Ось ще одне питання, яке забезпечує хороше порівняння.)

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> Список </code>, але якщо є інші цікаві способи роботи з, скажімо, колекцією <code> </code>, я б цікаво їх знати.
iX3

@DaveNewton, дякую за ідею. Я ніколи нічого подібного не використовував. Будь ласка, погляньте на моє відредаговане запитання і дайте мені знати, чи я зрозумів, що ви мали на увазі.
iX3

2
@sdasdadas, зроблено: stackoverflow.com/questions/18410035 / ...
ix3

Відповіді:


265

Три форми циклічного циклу майже однакові. Розширений forцикл:

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

відповідно до специфікації мови Java , ідентична за фактичним використанням ітератора з традиційним forциклом. У третьому випадку ви можете змінити вміст списку, лише видаливши поточний елемент, і тоді, лише якщо це зробити через removeметод самого ітератора. Завдяки ітерації на основі індексу ви можете будь-яким чином змінювати список. Однак додавання або видалення елементів, що стають перед поточним індексом, ризикує пропустити елементи циклу або обробити один і той же елемент кілька разів; вам потрібно правильно відрегулювати індекс циклу під час внесення таких змін.

У всіх випадках elementце посилання на фактичний елемент списку. Жоден з методів ітерації не робить копію нічого в списку. Зміни у внутрішньому стані elementбуде завжди бачитись у внутрішньому стані відповідного елемента у списку.

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

EDIT: Як у коментарі вказує @ iX3, ви можете використовувати a ListIteratorдля встановлення поточного елемента списку під час ітерації. Вам потрібно використовувати List#listIterator()замість того, List#iterator()щоб ініціалізувати змінну циклу (що, очевидно, потрібно було б оголосити ListIteratorне an Iterator).


Добре, дякую, але якщо ітератор фактично повертає посилання на реальний елемент (а не копію), то як я можу використовувати його для зміни значення у списку? Я вважаю, що якщо я використовую e = iterator.next()тоді, e = somethingElseпросто змінюю те, про що eйдеться, а не змінює фактичний магазин, з якого iterator.next()отримано значення.
iX3

@ iX3 - Це було б правдою і для інтерактивних ітерацій; призначення нового об’єкта eне змінює те, що в списку; ви повинні зателефонувати list.set(index, thing). Ви можете змінити вміст з e(наприклад, e.setSomething(newValue)), але для зміни того, що елемент зберігається в списку , як ви ітерацію, ви повинні дотримуватися індексу на основі ітерації.
Тед Хопп

Дякую за пояснення; Думаю, я розумію, що ви зараз говорите, і відповідно оновлю моє запитання / коментарі. Немає "копіювання" per-se, але через мовну конструкцію, щоб внести зміни до вмісту, eя повинен був би зателефонувати одному із eметодів, тому що призначення просто змінює вказівник (вибачте мій С).
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());
     }
}

23

Базовий цикл не рекомендується, оскільки ви не знаєте реалізації списку.

Якщо це був LinkedList, кожен дзвінок на

list.get(i)

буде повторювати список, що призведе до складності N ^ 2 у часі.


1
Правильно; це хороший момент, і я оновлю приклад у питанні.
iX3

Відповідно до цієї публікації, ваше твердження є невірним: що є більш ефективним, для кожного циклу чи ітератором?
Hibbem

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

Дякую, я маю намір оновити список вгорі з рішеннями Java 8 з часом.
iX3

1
@ eugene82 чи набагато ефективніший такий підхід?
nazar_art

1
@nazar_art Ви не побачите значного вдосконалення в порівнянні з будь-яким іншим, якщо ви, можливо, не використовували paralStream у дуже великій колекції. Це більш ефективно лише в сенсі зцілення багатослівності, насправді.
Rogue

7

У Java 8 у нас є кілька способів ітерації над колекційними класами.

Використання Iterable forEach

Колекції, які реалізують Iterable(наприклад, усі списки), тепер мають forEachметод. Ми можемо використовувати методи-довідки, введені в Java 8.

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

Використання потоків forEach та forEachOrряд

Ми також можемо повторити список, використовуючи 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);

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

@ iX3 Не хвилюйся; Я просто жартував. Шляхи - це списки: "почати в корені" (очевидно), "перейти до другої дитини", "перейти до 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 (). Це також допомагає написати внутрішній цикл, який порівнює дві позиції у списку (ітератори не рівні).


0

Правильно, перераховано багато альтернатив. Найпростішим і найпростішим було б просто використання вдосконаленого forоператора, як показано нижче. ExpressionМає певний тип , який ітерацію.

for ( FormalParameter : Expression ) Statement

Наприклад, для повторення ідентифікаторів, Список <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);
                }
        );
    }
}

Класно. Чи можете ви пояснити, чим це відрізняється від eugene82відповіді i_am_zero'' і '' ?
iX3

@ iX3 ах. Не прочитав увесь список відповідей. Намагався бути корисним .. Ви хочете, щоб я видалив відповідь?
Результати пошуку Веб-результати Pi

Для мене це не має значення, хоча, можливо, ви могли б вдосконалити його, порівнявши та порівнявши його з іншими методами. Або якщо ви вважаєте, що це не демонструє жодної нової методики, але все-таки може бути корисною як орієнтир для інших, можливо, ви можете додати примітку, що пояснює це.
iX3

-2

Ви завжди можете вимкнути перший і третій приклади з циклом час і трохи більше коду. Це дає вам перевагу в можливості використання часу:

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

Звичайно, подібні речі можуть спричинити NullPointerException, якщо list.size () повертає 0, тому що він завжди виконується хоча б раз. Це можна виправити, перевіривши, якщо елемент недійсний перед тим, як використовувати його атрибути / методи tho. Тим не менш, це набагато простіше і простіше використовувати для циклу


Щоправда ... Я здогадуюсь, що це технічно інше, але поведінка відрізняється лише в тому випадку, якщо список порожній, правда?
iX3

@ ix3 так, і в цьому випадку поведінка гірша. Я б не назвав це гарною альтернативою.
CPerkins

Звичайно, але, чесно кажучи, це питання менше про те, що "добре", а більше про те, що "можливо", тому я все одно ціную цю відповідь.
iX3
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.