Является ли 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"));