Чи забезпечує Java 8 хороший спосіб повторити значення чи функцію?


118

У багатьох інших мовах, наприклад. Haskell, легко повторити значення або функцію кілька разів, наприклад. щоб отримати список з 8 примірників значення 1:

take 8 (repeat 1)

але я ще не знайшов цього в Java 8. Чи є така функція в JDK Java 8?

Або ж щось еквівалентне такому діапазону

[1..8]

Здавалося б, очевидною заміною для багатослівного твердження на Java

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

мати щось подібне

Range.from(1, 8).forEach(i -> System.out.println(i))

хоча цей конкретний приклад насправді не виглядає набагато більш стислим ... але, сподіваємось, він є більш читабельним.


2
Ви вивчали API Streams ? Це має бути найкращим вашим ставкою, що стосується JDK. Він має функцію діапазону , саме це я знайшов досі.
Марко Тополник

1
@MarkoTopolnik Клас Streams був видалений (точніше, він був розділений між декількома іншими класами, а деякі методи були повністю видалені).
assylias

3
Ви викликаєте багатослівний цикл! Це добре, що вас не було поруч у дні Кобола. Для відображення висхідних чисел було прийнято понад 10 декларативних заяв у Коболі. Молодь в ці дні не оцінює, наскільки їм це добре.
Гілберт Ле Блан

1
@GilbertLeBlanc багатослов'я не має нічого спільного. Петлі не композиційні, Потоки є. Цикли призводять до неминучого повторення, тоді як потоки дозволяють повторно використовувати. Оскільки такі потоки є кількісно кращою абстракцією, ніж петлі, і слід віддавати перевагу.
Ален О'Дея

2
@GilbertLeBlanc і нам довелося кодувати босими ногами, на снігу.
Давуд ібн Карім

Відповіді:


155

Для цього конкретного прикладу ви можете:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

Якщо вам потрібен крок, відмінний від 1, ви можете використовувати функцію відображення, наприклад, для кроку 2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

Або створити власну ітерацію та обмежити розмір ітерації:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);

4
Закриття повністю перетворить код Java на краще. З нетерпінням чекаю цього дня ...
Marko Topolnik

1
@jwenting Це дійсно залежить - зазвичай це стосується графічного інтерфейсу (Swing або JavaFX), який видаляє багато плит котла через анонімні класи.
assylias

8
@jwenting Для всіх, хто має досвід роботи в FP, код, який обертається навколо функцій вищого порядку, - це чистий виграш. Для тих, хто не має цього досвіду, прийшов час оновити свої навички --- або ризикувати залишитися в пилу.
Марко Топольник

2
@MarkoTopolnik Можливо, ви хочете скористатися дещо новішою версією javadoc (ви вказуєте на складання 78, найновіша - це збірка 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel

1
@GraemeMoss Ви все ще можете використовувати той самий шаблон ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());), але він плутає річ IMO, і в цьому випадку петля здається вказаною.
assylias

65

Ось ще одна техніка, яку я натрапив на днях:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

Collections.nCopiesВиклик створює Listмістять nкопії будь-яке значення , ви надаєте. У цьому випадку це Integerзначення в коробці 1. Звичайно, він фактично не створює список з nелементами; він створює "віртуалізований" список, що містить лише значення та довжину, і будь-який виклик в getмежах діапазону просто повертає значення. nCopiesМетод був навколо з Колекцією рамкової був введений ще в JDK 1.2. Звичайно, можливість створення потоку з його результату була додана в Java SE 8.

Велика справа, ще один спосіб зробити те ж саме приблизно в однаковій кількості рядків.

Однак ця методика швидша за IntStream.generateта IntStream.iterateнаближається, і на диво, вона також швидша за IntStream.rangeпідхід.

Бо iterateі generateрезультат, мабуть, не надто дивний. Рамка потоків (насправді сплітератори для цих потоків) побудована на припущенні, що лямбди потенційно можуть генерувати різні значення кожного разу, і що вони будуть генерувати необмежену кількість результатів. Це робить паралельне розщеплення особливо складним. iterateМетод також проблематичний для цього випадку , тому що кожен виклик вимагає результату попередньої. Таким чином, потоки, що використовують generateі iterateне дуже добре для отримання повторних констант.

Відносно погані показники роботи rangeдивують. Це теж віртуалізується, тому елементи насправді не всі існують у пам'яті, а розмір відомий наперед. Це повинно створити швидкий і легко паралельний сплітератор. Але це дивно не дуже добре. Можливо, причина полягає в тому, що rangeнеобхідно обчислити значення для кожного елемента діапазону, а потім викликати функцію на ньому. Але ця функція просто ігнорує вхід і повертає константу, тому я здивований, що це не вбудовано і не вбито.

Collections.nCopiesТехніка повинна зробити бокс / розпакування для того , щоб обробляти значення, так як немає примітивних спеціалізацій List. Оскільки значення є однаковим щоразу, воно в основному є одним кодом, і це поле ділиться всіма nкопіями. Я підозрюю, що бокс / розблокування дуже оптимізований, навіть непростий, і це може бути добре накреслено.

Ось код:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

Ось результати JMH: (2,8 ГГц Core2Duo)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

У версії ncopies є досить велика дисперсія, але в цілому вона здається комфортно на 20 разів швидшою, ніж версія діапазону. (Я б дуже хотів вірити, що я щось не так зробив.)

Я здивований тим, наскільки добре nCopiesпрацює техніка. Всередині він не особливо особливий, при цьому потік віртуалізованого списку просто реалізується за допомогою IntStream.range! Я очікував, що потрібно буде створити спеціалізований розширювач, щоб це швидко пройшло, але це вже здається досить непоганим.


6
Менш досвідчені розробники можуть заплутатися або потрапити в проблеми, коли дізнаються, що nCopiesнасправді нічого не копіює, а "копії" вказують на цей єдиний об'єкт. Це завжди безпечно, якщо цей об'єкт є непорушним , наприклад, коробчатий примітив у цьому прикладі. Ви натякаєте на це у своїй заяві "одного разу в коробці", але може бути непогано виразно закликати до цього застереження, оскільки така поведінка не характерна для автоматичного боксу.
Вільям Прайс

1
Отже, це означає, що LongStream.rangeце значно повільніше, ніж IntStream.range? Тож добре, що ідея не пропонувати IntStream(а використовувати LongStreamдля всіх цілих типів) відпала. Зауважте, що для випадку послідовного використання взагалі немає причин використовувати потік: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));чи те саме, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));але ще ефективніше може бутиCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Холгер

1
@Holger, ці випробування проводилися на профілі чистого типу, тому вони не мають відношення до реального світу. Ймовірно, LongStream.rangeпрацює гірше, тому що у нього є дві карти LongFunctionзсередини, тоді ncopiesяк три карти з IntFunction, ToLongFunctionі LongFunction, таким чином, всі лямбди є мономорфними. Виконання цього тесту на попередньо забрудненому профілі типу (який ближче до реального випадку) показує, що ncopiesце на 1,5 рази повільніше.
Тагір Валєєв

1
Передчасна оптимізація FTW
Рафаель Бугаєвський

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

35

Для повноти, а також тому, що я не міг собі допомогти :)

Генерування обмеженої послідовності констант досить близьке до того, що ви бачили б у Haskell, тільки при багатомовності на рівні Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);

() -> 1генерує лише 1, це призначено? Тож вихід був би 1 1 1 1 1 1 1 1.
Крістіан Улленбум

4
Так, за першим прикладом Haskell ОП take 8 (repeat 1). Ассілія в значній мірі охоплювала всі інші випадки.
clstrfsck

3
Stream<T>також є загальний generateметод отримання нескінченного потоку якогось іншого типу, який можна обмежити так само.
zstewart

11

Як тільки функція повторення десь визначається як

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

Ви можете використовувати це час від часу, наприклад:

repeat.accept(8, () -> System.out.println("Yes"));

Отримати і еквівалент Haskell's

take 8 (repeat 1)

Ви могли написати

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));

2
Цей дивовижний. Однак я змінив його, щоб вказати номер ітерації назад, змінивши Runnableна Function<Integer, ?>і потім скориставшись f.apply(i).
Fons

0

Це моє рішення щодо реалізації функції часу. Я молодший, тому, я визнаю, це може бути не ідеально, я би радий почути, якщо це не є хорошою ідеєю з якоїсь причини.

public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
    while (count > 0) {
        f.apply(t);
        count--;
    }
    return null;
}

Ось кілька прикладів використання:

Function<String, Void> greet = greeting -> {
    System.out.println(greeting);
    return null;
};

times(3, greet, "Hello World!");
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.