Предоставляет ли 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
Вы изучали Streams API ? Это должно быть вашим лучшим выбором для JDK. У него есть функция диапазона , это то, что я нашел до сих пор.
Марко Топольник
1
@MarkoTopolnik Класс Streams был удален (точнее, он был разделен между несколькими другими классами, а некоторые методы были полностью удалены).
assylias
3
Вы вызываете подробный цикл for! Хорошо, что тебя не было во времена Кобола. Для отображения возрастающих чисел в Cobol потребовалось более 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);
assylias
источник
4
Замыкания полностью изменят код Java к лучшему. С нетерпением жду того дня ...
Марко Топольник
1
@jwenting Это действительно зависит - обычно с графическим интерфейсом пользователя (Swing или JavaFX), который удаляет множество шаблонов из-за анонимных классов.
assylias
8
@jwenting Для любого, у кого есть опыт работы с FP, код, который вращается вокруг функций высшего порядка, является чистой победой. Тем, у кого нет такого опыта, пришло время повысить свои навыки - иначе рискуете остаться в тени.
Марко Топольник
2
@MarkoTopolnik Возможно, вы захотите использовать немного более новую версию javadoc (вы указываете на сборку 78, последняя - сборка 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Марк Роттевил
1
@GraemeMoss Вы все равно можете использовать тот же шаблон ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());), но это сбивает с толку ИМО, и в этом случае цикл кажется указанным.
assylias
65

Вот еще одна техника, с которой я столкнулся на днях:

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

Collections.nCopiesВызов создает Listсодержащие nкопии любое значение , вы предоставляете. В данном случае это Integerзначение в рамке 1. Конечно, на самом деле он не создает список с nэлементами; он создает «виртуализированный» список, содержащий только значение и длину, и любой вызов в getпределах диапазона просто возвращает значение. Этот nCopiesметод существует с тех пор, как Collections Framework был представлен еще в 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: (Core2Duo 2,8 ГГц)

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цикл значительно превзойдет любой из них.
typeracer
35

Для полноты, а также потому, что я ничего не мог с собой поделать :)

Генерация ограниченной последовательности констант довольно близка к тому, что вы видели бы в Haskell, только с детализацией уровня Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck
источник
() -> 1будет генерировать только единицы, это предназначено? Так что на выходе будет 1 1 1 1 1 1 1 1.
Christian Ullenboom
4
Да, согласно первому примеру OP Haskell take 8 (repeat 1). assylias в значительной степени покрывает все остальные случаи.
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!");
JH
источник