Является ли Java 8 хорошим способом повторить значение или функцию?
Во многих других языках, например. Haskell, легко повторить значение или функцию несколько раз, например. для получения списка из 8 копий значения 1:
take 8 (repeat 1)
но я еще не нашел это в Java 8. Есть ли такая функция в Java 8 JDK?
Или, альтернативно, что-то эквивалентное диапазону, например
[1..8]
Казалось бы очевидной заменой подробного утверждения в Java, например
for (int i = 1; i <= 8; i++) {
System.out.println(i);
}
иметь что-то вроде
Range.from(1, 8).forEach(i -> System.out.println(i))
хотя этот конкретный пример не выглядит намного более кратким на самом деле... но, надеюсь, он более читабельный.
Ответы
Ответ 1
Для этого конкретного примера вы можете сделать:
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);
Ответ 2
Здесь другая техника, с которой я столкнулся через день:
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
результат, возможно, не слишком удивителен. Структура потоков (на самом деле, Spliterators для этих потоков) построена на предположении, что lambdas будет потенциально генерировать разные значения каждый раз и что они будут генерировать неограниченное количество результатов. Это затрудняет параллельное расщепление. Метод 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.8GHz 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
! Я ожидал, что будет необходимо создать специализированный spliterator, чтобы заставить это идти быстро, но это уже кажется довольно хорошим.
Ответ 3
Для полноты, а также потому, что я не мог с собой поделать:)
Создание ограниченной последовательности констант довольно близко к тому, что вы увидите в Haskell, только с многословностью уровня Java.
IntStream.generate(() -> 1)
.limit(8)
.forEach(System.out::println);
Ответ 4
Как только функция повторения где-то определена как
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
take 8 (repeat 1)
Вы можете написать
StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));