Java SE 8 a-t-il des paires ou des tuples?


185

Je joue avec des opérations fonctionnelles paresseuses dans Java SE 8, et je veux mapun index ivers une paire / un tuple (i, value[i]), puis filterbasé sur le deuxième value[i]élément, et finalement ne sortir que les indices.

Dois-je encore souffrir ceci: Quel est l'équivalent de la paire C ++ <L, R> en Java? dans la nouvelle ère audacieuse des lambdas et des ruisseaux?

Mise à jour: j'ai présenté un exemple plutôt simplifié, qui a une solution soignée proposée par @dkatzel dans l'une des réponses ci-dessous. Cependant, il ne se généralise pas . Par conséquent, permettez-moi d'ajouter un exemple plus général:

package com.example.test;

import java.util.ArrayList;
import java.util.stream.IntStream;

public class Main {

  public static void main(String[] args) {
    boolean [][] directed_acyclic_graph = new boolean[][]{
        {false,  true, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false, false}
    };

    System.out.println(
        IntStream.range(0, directed_acyclic_graph.length)
        .parallel()
        .mapToLong(i -> IntStream.range(0, directed_acyclic_graph[i].length)
            .filter(j -> directed_acyclic_graph[j][i])
            .count()
        )
        .filter(n -> n == 0)
        .collect(() -> new ArrayList<Long>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
    );
  }

}

Cela donne une sortie incorrecte[0, 0, 0] qui correspond aux décomptes pour les trois colonnes qui sont toutes false. Ce dont j'ai besoin, ce sont les indices de ces trois colonnes. La sortie correcte doit être [0, 2, 4]. Comment puis-je obtenir ce résultat?


2
Il y a déjà AbstractMap.SimpleImmutableEntry<K,V>depuis des années ... Mais de toute façon, au lieu de la cartographie ià (i, value[i])juste pour filtrer par value[i]et cartographie Retour à i: pourquoi ne pas simplement filtrer par value[i]en premier lieu, sans la mise en correspondance?
Holger

@Holger J'ai besoin de savoir quels indices d'un tableau contiennent des valeurs qui correspondent à un critère. Je ne peux pas le faire sans conserver idans le flux. J'ai également besoin value[i]des critères. C'est pourquoi j'ai besoin(i, value[i])
nécromancien

1
@necromancer D'accord, cela ne fonctionne que s'il est bon marché d'obtenir la valeur de l'index, comme un tableau, une collection à accès aléatoire ou une fonction peu coûteuse. Je suppose que le problème est que vous vouliez présenter un cas d'utilisation simplifié, mais il a été trop simplifié et a donc succombé à un cas particulier.
Stuart marque le

1
@necromancer J'ai édité un peu le dernier paragraphe pour clarifier la question que je pense que vous posez. Est ce juste? Est-ce aussi une question sur un graphe orienté (non acyclique)? (Pas que cela compte beaucoup.) Enfin, le résultat souhaité doit-il être [0, 2, 4]?
Stuart marque le

1
Je pense que la bonne solution pour résoudre ce problème est d'avoir une future version Java prenant en charge les tuples comme type de retour (comme cas particulier d'Object) et que les expressions lambda puissent utiliser un tel tuple directement pour ses paramètres.
Thorbjørn Ravn Andersen

Réponses:


206

MISE À JOUR: Cette réponse est en réponse à la question initiale, Java SE 8 a-t-il des paires ou des tuples? (Et implicitement, sinon, pourquoi?) L'OP a mis à jour la question avec un exemple plus complet, mais il semble que cela puisse être résolu sans utiliser aucun type de structure Pair. [Note d'OP: voici l'autre bonne réponse .]


La réponse courte est non. Vous devez soit lancer le vôtre, soit apporter l'une des nombreuses bibliothèques qui l'implémentent.

Avoir une Pairclasse en Java SE a été proposé et rejeté au moins une fois. Consultez ce fil de discussion sur l'une des listes de diffusion OpenJDK. Les compromis ne sont pas évidents. D'une part, il existe de nombreuses implémentations Pair dans d'autres bibliothèques et dans le code d'application. Cela démontre un besoin, et l'ajout d'une telle classe à Java SE augmentera la réutilisation et le partage. D'un autre côté, avoir une classe Pair ajoute à la tentation de créer des structures de données compliquées à partir de paires et de collections sans créer les types et abstractions nécessaires. (C'est une paraphrase du message de Kevin Bourillion à partir de ce fil.)

Je recommande à tout le monde de lire l'intégralité de ce fil de discussion. C'est remarquablement perspicace et n'a pas de flamage. C'est assez convaincant. Quand il a commencé, j'ai pensé, "Ouais, il devrait y avoir une classe Pair dans Java SE" mais au moment où le thread a atteint sa fin, j'avais changé d'avis.

Notez cependant que JavaFX a la classe javafx.util.Pair . Les API JavaFX ont évolué séparément des API Java SE.

Comme on peut le voir dans la question liée Quel est l'équivalent de la paire C ++ en Java?il y a un espace de conception assez grand autour de ce qui est apparemment une API si simple. Les objets doivent-ils être immuables? Devraient-ils être sérialisables? Devraient-ils être comparables? Le cours doit-il être final ou non? Faut-il ordonner les deux éléments? Doit-il être une interface ou une classe? Pourquoi s'arrêter aux paires? Pourquoi pas des triples, des quads ou des N-tuples?

Et bien sûr, il y a l'inévitable dénomination bikeshed pour les éléments:

  • (un B)
  • (première seconde)
  • (gauche droite)
  • (voiture, cdr)
  • (toto, bar)
  • etc.

Un gros problème qui a à peine été mentionné est la relation des paires avec les primitifs. Si vous avez une (int x, int y)donnée qui représente un point dans l'espace 2D, la représenter comme Pair<Integer, Integer>consomme trois objets au lieu de deux mots de 32 bits. En outre, ces objets doivent résider sur le tas et entraîneront une surcharge GC.

Il semblerait clair que, comme les Streams, il serait essentiel qu'il y ait des spécialisations primitives pour les Paires. Voulons-nous voir:

Pair
ObjIntPair
ObjLongPair
ObjDoublePair
IntObjPair
IntIntPair
IntLongPair
IntDoublePair
LongObjPair
LongIntPair
LongLongPair
LongDoublePair
DoubleObjPair
DoubleIntPair
DoubleLongPair
DoubleDoublePair

Même un IntIntPairnécessiterait toujours un objet sur le tas.

Celles-ci rappellent bien sûr la prolifération des interfaces fonctionnelles dans le java.util.functionpackage de Java SE 8. Si vous ne voulez pas d'une API surchargée, lesquelles laisseriez-vous de côté? Vous pourriez également soutenir que cela ne suffit pas et que des spécialisations, par exemple, Booleandevraient également être ajoutées.

Mon sentiment est que si Java avait ajouté une classe Pair il y a longtemps, cela aurait été simple, voire simpliste, et cela n'aurait pas satisfait la plupart des cas d'utilisation que nous envisageons actuellement. Considérez que si Pair avait été ajouté dans la période de temps JDK 1.0, il aurait probablement été mutable! (Regardez java.util.Date.) Est-ce que les gens auraient été satisfaits de cela? Je suppose que s'il y avait une classe Pair en Java, ce ne serait pas vraiment utile et tout le monde roulera toujours la sienne pour satisfaire ses besoins, il y aurait diverses implémentations Pair et Tuple dans des bibliothèques externes, et les gens continueraient à se disputer / discuter de la manière de réparer la classe Pair de Java. En d'autres termes, un peu au même endroit où nous en sommes aujourd'hui.

Pendant ce temps, des travaux sont en cours pour résoudre le problème fondamental, qui est une meilleure prise en charge dans la JVM (et éventuellement le langage Java) pour les types valeur . Consultez ce document sur l' état des valeurs . Il s'agit d'un travail préliminaire et spéculatif, et il ne couvre que les problèmes du point de vue de la JVM, mais il a déjà beaucoup de réflexion derrière cela. Bien sûr, il n'y a aucune garantie que cela entrera dans Java 9, ou jamais n'importe où, mais cela montre la direction actuelle de la réflexion sur ce sujet.


3
Les méthodes @necromancer Factory avec des primitives n'aident pas Pair<T,U>. Puisque les génériques doivent être de type référence. Toutes les primitives seront encadrées lors de leur stockage. Pour stocker des primitives, vous avez vraiment besoin d'une classe différente.
Stuart marque le

3
@necromancer Et oui rétrospectivement, les constructeurs primitifs encadrés n'auraient pas dû être publics, et valueOfauraient dû être le seul moyen d'obtenir une instance encadrée. Mais ceux-ci sont là depuis Java 1.0 et ne valent probablement pas la peine d'être modifiés à ce stade.
Stuart marque le

3
Évidemment, il ne devrait y avoir qu'un seul public Pairou Tupleclasse avec une méthode de fabrique créant les classes de spécialisation nécessaires (avec un stockage optimisé) de manière transparente en arrière-plan. En fin de compte, les lambdas font exactement cela: ils peuvent capturer un nombre arbitraire de variables de type arbitraire. Et maintenant l'image d'un support de langage permettant de créer la classe de tuple appropriée au moment de l'exécution déclenchée par une invokedynamicinstruction…
Holger

3
@Holger Quelque chose comme ça pourrait fonctionner si l'on mettait à jour des types de valeur sur la JVM existante, mais la proposition de types de valeur (maintenant "Projet Valhalla" ) est beaucoup plus radicale. En particulier, ses types valeur ne seraient pas nécessairement alloués en tas. Aussi, contrairement aux objets d'aujourd'hui, et comme les primitifs aujourd'hui, les valeurs n'auraient pas d'identité.
Stuart marque le

2
@Stuart Marks: Cela n'interférerait pas car le type que j'ai décrit pourrait être le type «encadré» pour un tel type de valeur. Avec une invokedynamicusine basée similaire à la création lambda, une telle modernisation ultérieure ne poserait aucun problème. À propos, les lambdas n'ont pas non plus d'identité. Comme indiqué explicitement, l'identité que vous pourriez percevoir aujourd'hui est un artefact de l'implémentation actuelle.
Holger

46

Vous pouvez jeter un œil sur ces classes intégrées:


3
C'est la bonne réponse, en ce qui concerne la fonctionnalité intégrée pour les paires. Notez que cela SimpleImmutableEntrygarantit uniquement que les références stockées dans le Entryne changent pas, pas que les champs des objets liés keyet value(ou ceux des objets auxquels ils sont liés ) ne changent pas.
Luke Hutchison

22

Malheureusement, Java 8 n'a pas introduit de paires ou de tuples. Vous pouvez toujours utiliser org.apache.commons.lang3.tuple bien sûr (que j'utilise personnellement en combinaison avec Java 8) ou vous pouvez créer vos propres wrappers. Ou utilisez Maps. Ou des trucs comme ça, comme expliqué dans la réponse acceptée à cette question à laquelle vous avez lié.


MISE À JOUR: JDK 14 introduit les enregistrements comme fonction de prévisualisation. Ce ne sont pas des tuples, mais ils peuvent être utilisés pour sauver la plupart des mêmes problèmes. Dans votre exemple spécifique ci-dessus, cela pourrait ressembler à ceci:

public class Jdk14Example {
    record CountForIndex(int index, long count) {}

    public static void main(String[] args) {
        boolean [][] directed_acyclic_graph = new boolean[][]{
                {false,  true, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false, false}
        };

        System.out.println(
                IntStream.range(0, directed_acyclic_graph.length)
                        .parallel()
                        .mapToObj(i -> {
                            long count = IntStream.range(0, directed_acyclic_graph[i].length)
                                            .filter(j -> directed_acyclic_graph[j][i])
                                            .count();
                            return new CountForIndex(i, count);
                        }
                        )
                        .filter(n -> n.count == 0)
                        .collect(() -> new ArrayList<CountForIndex>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
        );
    }
}

Une fois compilé et exécuté avec JDK 14 (au moment de l'écriture, il s'agit d'une version à accès anticipé) à l'aide de l' --enable-previewindicateur, vous obtenez le résultat suivant:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]

En fait, l'une des réponses de @StuartMarks m'a permis de le résoudre sans tuples, mais comme cela ne semble pas se généraliser, j'en aurai probablement besoin éventuellement.
nécromancien

@necromancer Oui, c'est une très bonne réponse. La bibliothèque Apache peut toujours être utile à certains moments, mais tout dépend de la conception du langage Javas. Fondamentalement, les tuples devraient être des primitifs (ou similaires) pour fonctionner comme ils le font dans d'autres langues.
blalasaadri

1
Au cas où vous ne l'auriez pas remarqué, la réponse incluait ce lien extrêmement informatif: cr.openjdk.java.net/~jrose/values/values-0.html sur la nécessité et les perspectives de ces primitives, y compris les tuples.
nécromancien

17

Il semble que l'exemple complet puisse être résolu sans utiliser aucun type de structure Pair. La clé est de filtrer sur les index de colonne, avec le prédicat vérifiant la colonne entière, au lieu de mapper les index de colonne au nombre d' falseentrées dans cette colonne.

Le code qui fait cela est ici:

    System.out.println(
        IntStream.range(0, acyclic_graph.length)
            .filter(i -> IntStream.range(0, acyclic_graph.length)
                                  .noneMatch(j -> acyclic_graph[j][i]))
            .boxed()
            .collect(toList()));

Il en résulte une sortie [0, 2, 4]dont je pense que le résultat correct demandé par l'OP.

Notez également l' boxed()opération qui encadre les intvaleurs dans des Integerobjets. Cela permet d'utiliser le toList()collecteur préexistant au lieu d'avoir à écrire des fonctions de collecteur qui font la boxe elles-mêmes.


1
+1 as dans votre manche :) Cela ne se généralise toujours pas, non? C'était l'aspect le plus substantiel de la question car je m'attends à faire face à d'autres situations où un tel schéma ne fonctionnera pas (par exemple des colonnes avec pas plus de 3 valeurs true). En conséquence, j'accepterai votre autre réponse comme correcte, mais j'indiquerai également celle-ci! Merci beaucoup :)
necromancer

C'est correct mais en acceptant l'autre réponse du même utilisateur. (voir les commentaires ci-dessus et ailleurs.)
necromancer

1
@necromancer Exactement, cette technique n'est pas tout à fait générale dans les cas où vous voulez l'index, mais l'élément de données ne peut pas être récupéré ou calculé à l'aide de l'index. (Du moins pas facilement.) Par exemple, considérons un problème où vous lisez des lignes de texte à partir d'une connexion réseau, et vous voulez trouver le numéro de ligne de la Nième ligne qui correspond à un modèle. Le moyen le plus simple consiste à mapper chaque ligne dans une paire ou une structure de données composite pour numéroter les lignes. Il y a probablement un moyen piraté et efficace de le faire sans une nouvelle structure de données.
Stuart marque le

@StuartMarks, Une paire est <T, U>. un triple <T, U, V>. etc. Votre exemple est une liste, pas une paire.
Pacerier

7

Vavr (anciennement appelé Javaslang) ( http://www.vavr.io ) fournit également des tuples (jusqu'à une taille de 8). Voici le javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Voici un exemple simple:

Tuple2<Integer, String> entry = Tuple.of(1, "A");

Integer key = entry._1;
String value = entry._2;

Pourquoi JDK lui-même n'est pas venu avec un type simple de tuples jusqu'à maintenant est un mystère pour moi. L'écriture de classes wrapper semble être une affaire de tous les jours.


Certaines versions de vavr utilisaient des lancers sournois sous le capot. Veillez à ne pas les utiliser.
Thorbjørn Ravn Andersen

7

Depuis Java 9, vous pouvez créer des instances Map.Entryplus faciles qu'auparavant:

Entry<Integer, String> pair = Map.entry(1, "a");

Map.entryrenvoie un non modifiable Entryet interdit les valeurs nulles.


6

Puisque vous ne vous souciez que des index, vous n'avez pas du tout besoin de mapper aux tuples. Pourquoi ne pas simplement écrire un filtre qui utilise les éléments de recherche dans votre tableau?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));

+1 pour une excellente solution pratique. Cependant, je ne suis pas sûr que cela se généralise à ma situation où je génère les valeurs à la volée. J'ai posé ma question sous forme de tableau pour offrir un cas simple à réfléchir et vous avez trouvé une excellente solution.
nécromancien

5

Oui.

Map.Entrypeut être utilisé comme un Pair.

Malheureusement, cela n'aide pas avec les flux Java 8 car le problème est que même si les lambdas peuvent prendre plusieurs arguments, le langage Java ne permet de renvoyer qu'une seule valeur (objet ou type primitif). Cela implique que chaque fois que vous avez un flux, vous vous retrouvez avec un seul objet de l'opération précédente. C'est un manque dans le langage Java, car si plusieurs valeurs de retour étaient prises en charge ET que les flux les prenaient en charge, nous pourrions avoir des tâches non triviales beaucoup plus agréables effectuées par les flux.

Jusque-là, il n'y a que peu d'utilité.

EDIT 2018-02-12: En travaillant sur un projet, j'ai écrit une classe d'assistance qui aide à gérer le cas particulier d'avoir un identifiant plus tôt dans le flux dont vous avez besoin ultérieurement, mais la partie de flux entre les deux ne le sait pas. Jusqu'à ce que je parvienne à le publier seul, il est disponible sur IdValue.java avec un test unitaire sur IdValueTest.java


2

Eclipse Collections a Pairet toutes les combinaisons de paires primitives / objets (pour les huit primitives).

La Tuplesfabrique peut créer des instances de Pair, et la PrimitiveTuplesfabrique peut être utilisée pour créer toutes les combinaisons de paires primitives / objets.

Nous les avons ajoutés avant la sortie de Java 8. Ils ont été utiles pour implémenter des itérateurs clé / valeur pour nos cartes primitives, que nous prenons également en charge dans toutes les combinaisons primitives / objets.

Si vous êtes prêt à ajouter la surcharge supplémentaire de la bibliothèque, vous pouvez utiliser la solution acceptée de Stuart et collecter les résultats dans une primitive IntListpour éviter la boxe. Nous avons ajouté de nouvelles méthodes dans Eclipse Collections 9.0 pour permettre la Int/Long/Doublecréation de collections à partir de Int/Long/DoubleStreams.

IntList list = IntLists.mutable.withAll(intStream);

Remarque: je suis un committer pour les collections Eclipse.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.