¿Java 8 proporciona una buena forma de repetir un valor o función?

118

En muchos otros idiomas, p. Ej. Haskell, es fácil repetir un valor o una función varias veces, p. Ej. para obtener una lista de 8 copias del valor 1:

take 8 (repeat 1)

pero no he encontrado esto todavía en Java 8. ¿Existe tal función en el JDK de Java 8?

O alternativamente algo equivalente a un rango como

[1..8]

Parecería un reemplazo obvio para una declaración detallada en Java como

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

tener algo como

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

aunque este ejemplo en particular no parece mucho más conciso en realidad ... pero con suerte es más legible.

Musgo de Graeme
fuente
2
¿Has estudiado la API de Streams ? Esa debería ser su mejor apuesta en lo que respecta al JDK. Tiene una función de rango , eso es lo que he encontrado hasta ahora.
Marko Topolnik
1
@MarkoTopolnik La clase Streams se ha eliminado (más precisamente se ha dividido entre varias otras clases y algunos métodos se han eliminado por completo).
assylias
3
¡Llamas a un bucle for detallado! Es una suerte que no estuvieras en los días de Cobol. Se necesitaron más de 10 declaraciones declarativas en Cobol para mostrar números ascendentes. Los jóvenes de estos días no aprecian lo bien que lo tienen.
Gilbert Le Blanc
1
@GilbertLeBlanc la verbosidad no tiene nada que ver con eso. Los bucles no se pueden componer, las transmisiones sí. Los bucles conducen a una repetición inevitable, mientras que los flujos permiten la reutilización. Como tales, las corrientes son una abstracción cuantitativamente mejor que los bucles y deberían preferirse.
Alain O'Dea
2
@GilbertLeBlanc y tuvimos que codificar descalzos, en la nieve.
Dawood ibn Kareem

Respuestas:

155

Para este ejemplo específico, puede hacer:

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

Si necesita un paso diferente de 1, puede utilizar una función de mapeo, por ejemplo, para un paso de 2:

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

O cree una iteración personalizada y limite el tamaño de la iteración:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);
Assylias
fuente
4
Los cierres transformarán completamente el código Java, para mejor.
Esperando
1
@jwenting Realmente depende, generalmente con elementos de GUI (Swing o JavaFX), que elimina una gran cantidad de placa de caldera debido a clases anónimas.
assylias
8
@jwenting Para cualquiera con experiencia en FP, el código que gira en torno a funciones de orden superior es una pura victoria. Para cualquiera que no tenga esa experiencia, es hora de mejorar sus habilidades, o arriesgarse a quedarse atrás en el polvo.
Marko Topolnik
2
@MarkoTopolnik Es posible que desee utilizar una versión un poco más nueva de javadoc (apunta a la compilación 78, la última es la compilación 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel
1
@GraemeMoss Aún podría usar el mismo patrón ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) pero confunde algo en mi opinión y en ese caso parece indicado un bucle.
Assylias
65

Aquí hay otra técnica que encontré el otro día:

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

los Collections.nCopies llamada crea un Listcontenido de ncopias de cualquier valor que proporcione. En este caso, es el Integervalor en caja 1. Por supuesto, en realidad no crea una lista con nelementos; crea una lista "virtualizada" que contiene solo el valor y la longitud, y cualquier llamada a getdentro del rango solo devuelve el valor. El nCopiesmétodo ha existido desde que se introdujo el marco de colecciones en JDK 1.2. Por supuesto, la capacidad de crear una secuencia a partir de su resultado se agregó en Java SE 8.

Gran cosa, otra forma de hacer lo mismo en aproximadamente el mismo número de líneas.

Sin embargo, esta técnica es más rápido que el IntStream.generatey IntStream.iteratese acerca, y sorprendentemente, también es más rápido que el IntStream.rangeenfoque.

por iterate y generateel resultado quizás no sea demasiado sorprendente. El marco de transmisiones (en realidad, los divisores de estas transmisiones) se basa en la suposición de que las lambdas generarán potencialmente diferentes valores cada vez, y que generarán un número ilimitado de resultados. Esto hace que la división en paralelo sea particularmente difícil. El iteratemétodo también es problemático para este caso porque cada llamada requiere el resultado de la anterior. Entonces, los flujos que usan generatey iterateno funcionan muy bien para generar constantes repetidas.

El rendimiento relativamente bajo de rangees sorprendente. Esto también está virtualizado, por lo que no todos los elementos existen en la memoria, y el tamaño se conoce de antemano. Esto debería hacer un spliterator rápido y fácilmente paralelizable. Pero sorprendentemente no le fue muy bien. Quizás la razón es que rangetiene que calcular un valor para cada elemento del rango y luego llamar a una función sobre él. Pero esta función simplemente ignora su entrada y devuelve una constante, por lo que me sorprende que esto no esté alineado y eliminado.

La Collections.nCopiestécnica tiene que hacer boxing / unboxing para poder manejar los valores, ya que no existen especializaciones primitivas de List. Dado que el valor es el mismo cada vez, básicamente está encuadrado una vez y ese cuadro es compartido por todas las ncopias. Sospecho que el boxeo / unboxing está altamente optimizado, incluso intrinsificado, y puede insertarse bien.

Aquí está el código:

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

Y aquí están los resultados de 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

Hay una buena cantidad de variación en la versión ncopies, pero en general parece cómodamente 20 veces más rápida que la versión de rango. (Sin embargo, estaría dispuesto a creer que hice algo mal).

Me sorprende lo bien que funciona la nCopiestécnica. Internamente, no es muy especial, ya que el flujo de la lista virtualizada simplemente se implementa usando IntStream.range! Esperaba que fuera necesario crear un spliterator especializado para que esto fuera rápido, pero ya parece bastante bueno.

Stuart Marks
fuente
6
Los desarrolladores menos experimentados pueden confundirse o meterse en problemas cuando se dan cuenta de que en nCopiesrealidad no copia nada y que todas las "copias" apuntan a ese único objeto. Siempre es seguro si ese objeto es inmutable , como una primitiva en caja en este ejemplo. Usted hace alusión a esto en su declaración "encajonado una vez", pero sería bueno mencionar explícitamente las advertencias aquí porque ese comportamiento no es específico del auto-boxing.
William Price
1
¿Entonces eso implica que LongStream.rangees significativamente más lento que IntStream.range? Así que es bueno que se haya descartado la idea de no ofrecer un IntStream(pero usarlo LongStreampara todos los tipos de enteros). Tenga en cuenta que para el caso de uso secuencial, no hay ninguna razón para usar la transmisión en absoluto: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));hace lo mismo, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));pero podría ser incluso más eficienteCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger
1
@Holger, estas pruebas se realizaron en un perfil de tipo limpio, por lo que no están relacionadas con el mundo real. Probablemente LongStream.rangefuncione peor, porque tiene dos mapas con el LongFunctioninterior, mientras que ncopiestiene tres mapas con IntFunction, ToLongFunctiony LongFunction, por lo tanto, todas las lambdas son monomórficas. La ejecución de esta prueba en un perfil de tipo precontaminado (que está más cerca del caso del mundo real) muestra que ncopieses 1,5 veces más lento.
Tagir Valeev
1
Optimización prematura FTW
Rafael Bugajewski
1
En aras de la integridad, sería bueno ver un punto de referencia que compare ambas técnicas con un forciclo antiguo simple . Si bien su solución es más rápida que el Streamcódigo, supongo que un forbucle superaría a cualquiera de estos por un margen significativo.
typeracer
35

Para completar, y también porque no pude evitarlo :)

Generar una secuencia limitada de constantes está bastante cerca de lo que vería en Haskell, solo con nivel de detalle de Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck
fuente
() -> 1solo generaría 1, ¿está previsto? Entonces la salida sería 1 1 1 1 1 1 1 1.
Christian Ullenboom
4
Sí, según el primer ejemplo de Haskell del OP take 8 (repeat 1). Assylias cubrió prácticamente todos los demás casos.
clstrfsck
3
Stream<T>también tiene un generatemétodo genérico para obtener un flujo infinito de algún otro tipo, que puede limitarse de la misma manera.
zstewart
11

Una vez que una función de repetición se define en algún lugar como

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

Puede usarlo de vez en cuando de esta manera, por ejemplo:

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

Para obtener y equivalente a Haskell

take 8 (repeat 1)

Podrías escribir

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));
Hartmut P.
fuente
2
Este es asombroso. Sin embargo he modificado para proporcionar el número de la parte posterior iteración, cambiando la Runnablea Function<Integer, ?>y luego utilizando f.apply(i).
Fons
0

Esta es mi solución para implementar la función de tiempos. Soy un junior, así que admito que podría no ser ideal, me alegraría saber si esto no es una buena idea por cualquier motivo.

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

Aquí hay un ejemplo de uso:

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

times(3, greet, "Hello World!");
J H
fuente