Java 8 est-il un bon moyen de répéter une valeur ou une fonction?

118

Dans de nombreuses autres langues, par exemple. Haskell, il est facile de répéter une valeur ou une fonction plusieurs fois, par exemple. pour obtenir une liste de 8 copies de la valeur 1:

take 8 (repeat 1)

mais je ne l'ai pas encore trouvé dans Java 8. Existe-t-il une telle fonction dans le JDK de Java 8?

Ou bien quelque chose d'équivalent à une gamme comme

[1..8]

Cela semblerait un remplacement évident pour une déclaration verbeuse en Java comme

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

avoir quelque chose comme

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

bien que cet exemple particulier ne semble pas beaucoup plus concis en fait ... mais j'espère qu'il est plus lisible.

Graeme Moss
la source
2
Avez-vous étudié l' API Streams ? Cela devrait être votre meilleur pari en ce qui concerne le JDK. Il a une fonction de plage , c'est ce que j'ai trouvé jusqu'à présent.
Marko Topolnik
1
@MarkoTopolnik La classe Streams a été supprimée (plus précisément, elle a été divisée entre plusieurs autres classes et certaines méthodes ont été complètement supprimées).
assylias
3
Vous appelez une boucle for verbeuse! C'est une bonne chose que vous n'étiez pas là à l'époque de Cobol. Il a fallu plus de 10 instructions déclaratives en Cobol pour afficher des nombres ascendants. Les jeunes de nos jours n'apprécient pas à quel point ils l'ont.
Gilbert Le Blanc
1
La verbosité de @GilbertLeBlanc n'a rien à voir avec cela. Les boucles ne sont pas composables, les flux le sont. Les boucles conduisent à une répétition inévitable, tandis que les flux permettent la réutilisation. En tant que tels, les flux sont une meilleure abstraction quantitativement que les boucles et devraient être préférés.
Alain O'Dea du
2
@GilbertLeBlanc et nous avons dû coder pieds nus, dans la neige.
Dawood ibn Kareem

Réponses:

155

Pour cet exemple spécifique, vous pouvez faire:

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

Si vous avez besoin d'une étape différente de 1, vous pouvez utiliser une fonction de mappage, par exemple, pour une étape de 2:

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

Ou créez une itération personnalisée et limitez la taille de l'itération:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);
assylies
la source
4
Les fermetures transformeront complètement le code Java, pour le mieux. Dans l'attente de ce jour ...
Marko Topolnik
1
@jwenting Cela dépend vraiment - généralement avec des éléments d'interface graphique (Swing ou JavaFX), qui suppriment beaucoup de plaques chauffantes à cause des classes anonymes.
assylias
8
@jwenting Pour quiconque a de l'expérience en FP, un code qui tourne autour de fonctions d'ordre supérieur est une pure victoire. Pour quiconque n'a pas cette expérience, il est temps de mettre à niveau ses compétences - ou risquer d'être laissé pour compte.
Marko Topolnik
2
@MarkoTopolnik Vous voudrez peut-être utiliser une version légèrement plus récente du javadoc (vous pointez vers la build 78, la dernière est la build 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel
1
@GraemeMoss Vous pouvez toujours utiliser le même pattern ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) mais cela confond la chose IMO et dans ce cas une boucle semble indiquée.
assylias
65

Voici une autre technique que j'ai utilisée l'autre jour:

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

L' Collections.nCopiesappel crée une copie Listcontenant la nvaleur que vous fournissez. Dans ce cas, c'est la Integervaleur encadrée 1. Bien sûr, cela ne crée pas réellement une liste avec des néléments; il crée une liste «virtualisée» qui ne contient que la valeur et la longueur, et tout appel à l' getintérieur de la plage renvoie simplement la valeur. La nCopiesméthode existe depuis que le Framework de collections a été introduit dans le JDK 1.2. Bien sûr, la possibilité de créer un flux à partir de son résultat a été ajoutée dans Java SE 8.

Gros problème, une autre façon de faire la même chose dans à peu près le même nombre de lignes.

Cependant, cette technique est plus rapide que l' approche IntStream.generateet IntStream.iterate, et étonnamment, elle est également plus rapide que l' IntStream.rangeapproche.

Car iterateet generatele résultat n'est peut-être pas trop surprenant. Le framework de flux (en fait, les Spliterators pour ces flux) est construit sur l'hypothèse que les lambdas généreront potentiellement des valeurs différentes à chaque fois, et qu'ils généreront un nombre illimité de résultats. Cela rend la division parallèle particulièrement difficile. La iterateméthode est également problématique dans ce cas car chaque appel nécessite le résultat du précédent. Ainsi, les flux utilisant generateet iteratene fonctionnent pas très bien pour générer des constantes répétées.

La performance relativement médiocre de rangeest surprenante. Cela aussi est virtualisé, donc les éléments n'existent pas tous en mémoire et la taille est connue à l'avance. Cela devrait conduire à un séparateur rapide et facilement parallélisable. Mais étonnamment, cela n'a pas très bien fonctionné. Peut-être que la raison est qu'il rangefaut calculer une valeur pour chaque élément de la plage, puis appeler une fonction dessus. Mais cette fonction ignore simplement son entrée et renvoie une constante, donc je suis surpris que cela ne soit pas incorporé et tué.

La Collections.nCopiestechnique doit faire du boxing / unboxing afin de gérer les valeurs, car il n'y a pas de spécialisations primitives de List. Puisque la valeur est la même à chaque fois, elle est essentiellement encadrée une fois et cette boîte est partagée par toutes les ncopies. Je soupçonne que la boxe / unboxing est hautement optimisée, voire intrinsèque, et qu'elle peut être bien intégrée.

Voici le code:

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

Et voici les résultats 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

Il y a une bonne quantité de variance dans la version ncopies, mais dans l'ensemble, cela semble confortablement 20 fois plus rapide que la version de gamme. (Je serais tout à fait prêt à croire que j'ai fait quelque chose de mal, cependant.)

Je suis surpris de voir à quel point la nCopiestechnique fonctionne. En interne, cela ne fait pas grand-chose de spécial, le flux de la liste virtualisée étant simplement implémenté en utilisant IntStream.range! Je m'attendais à ce qu'il soit nécessaire de créer un séparateur spécialisé pour que cela aille rapidement, mais cela semble déjà être assez bon.

Marques Stuart
la source
6
Les développeurs moins expérimentés peuvent être confus ou avoir des problèmes lorsqu'ils apprennent que nCopiesrien ne copie réellement et que les "copies" pointent toutes vers cet objet unique. Il est toujours sûr si cet objet est immuable , comme une primitive encadrée dans cet exemple. Vous faites allusion à cela dans votre déclaration "boxed once", mais il pourrait être intéressant d'appeler explicitement les mises en garde ici car ce comportement n'est pas spécifique à l'auto-boxing.
William Price
1
Cela implique donc que LongStream.rangec'est beaucoup plus lent que IntStream.range? C'est donc une bonne chose que l'idée de ne pas proposer de IntStream(mais de l'utiliser LongStreampour tous les types d'entiers) ait été abandonnée. Notez que pour le cas d'utilisation séquentielle, il n'y a aucune raison d'utiliser stream: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));fait la même chose, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));mais peut-être encore plus efficaceCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger
1
@Holger, ces tests ont été effectués sur un profil de type propre, ils ne sont donc pas liés au monde réel. Probablement LongStream.rangeeffectue pire, car il a deux cartes avec l' LongFunctionintérieur, tout ncopiesa trois cartes avec IntFunction, ToLongFunctionet LongFunctiondonc tous les lambdas sont monomorphe. L'exécution de ce test sur un profil de type pré-pollué (qui est plus proche du cas réel) montre qu'il ncopiesest 1,5 fois plus lent.
Tagir Valeev
1
Optimisation prématurée FTW
Rafael Bugajewski
1
Par souci d'exhaustivité, il serait bien de voir un benchmark qui compare ces deux techniques avec une simple forboucle ancienne . Bien que votre solution soit plus rapide que le Streamcode, je suppose qu'une forboucle battrait l'un ou l'autre de ces éléments par une marge significative.
typeracer
35

Par souci d'exhaustivité, et aussi parce que je ne pouvais pas m'en empêcher :)

La génération d'une séquence limitée de constantes est assez proche de ce que vous verriez dans Haskell, uniquement avec un niveau de verbosité Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck
la source
() -> 1ne générerait que des 1, est-ce prévu? Donc, la sortie serait 1 1 1 1 1 1 1 1.
Christian Ullenboom
4
Oui, selon le premier exemple Haskell du PO take 8 (repeat 1). les assylies couvraient à peu près tous les autres cas.
clstrfsck
3
Stream<T>a également une generateméthode générique pour obtenir un flux infini d'un autre type, qui peut être limité de la même manière.
zstewart
11

Une fois qu'une fonction de répétition est quelque part définie comme

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

Vous pouvez l'utiliser de temps en temps de cette façon, par exemple:

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

Pour obtenir et équivalent à Haskell

take 8 (repeat 1)

Tu pourrais écrire

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));
Hartmut P.
la source
2
Celui-ci est génial. Cependant, je l'ai modifié pour fournir le numéro de l'itération en arrière, en changeant le Runnableen Function<Integer, ?>puis en utilisant f.apply(i).
Fons
0

C'est ma solution pour implémenter la fonction times. Je suis un junior donc j'admets que cela pourrait ne pas être idéal, je serais heureux d'apprendre si ce n'est pas une bonne idée pour une raison quelconque.

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

Voici quelques exemples d'utilisation:

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

times(3, greet, "Hello World!");
JH
la source