Hat Java SE 8 Paare oder Tupel?

183

Ich spiele mit faulen Funktionsoperationen in Java SE 8 herum und möchte map einen Index ifür ein Paar / Tupel erstellen (i, value[i]), dann filterbasierend auf dem zweiten value[i]Element, und schließlich nur die Indizes ausgeben.

Muss ich noch darunter leiden: Was entspricht dem C ++ - Paar <L, R> in Java? in der kühnen neuen Ära der Lambdas und Bäche?

Update: Ich habe ein ziemlich vereinfachtes Beispiel vorgestellt, das eine nette Lösung von @dkatzel in einer der folgenden Antworten enthält. Dies ist jedoch nicht der Fall verallgemeinert . Lassen Sie mich daher ein allgemeineres Beispiel hinzufügen:

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

}

Dies ergibt eine falsche Ausgabe, [0, 0, 0]die der Anzahl für die drei Spalten entspricht, die alle sind false. Was ich brauche, sind die Indizes dieser drei Spalten. Die richtige Ausgabe sollte sein [0, 2, 4]. Wie kann ich dieses Ergebnis erhalten?

Nekromant
quelle
2
Es ist schon AbstractMap.SimpleImmutableEntry<K,V>seit Jahren ... Aber wie auch immer, statt Zuordnung izu (i, value[i])nur zum Filtern von value[i]und Mapping zurück zu i: warum Filter nicht nur von value[i]in erster Linie, ohne die Zuordnung?
Holger
@Holger Ich muss wissen, welche Indizes eines Arrays Werte enthalten, die einem Kriterium entsprechen. Ich kann es nicht tun, ohne iim Stream zu bleiben. Ich brauche auch value[i]für die Kriterien. Deshalb brauche ich(i, value[i])
Nekromant
1
@necromancer Richtig, es funktioniert nur, wenn es billig ist, den Wert aus dem Index zu ermitteln, z. B. ein Array, eine Direktzugriffssammlung oder eine kostengünstige Funktion. Ich denke, das Problem ist, dass Sie einen vereinfachten Anwendungsfall präsentieren wollten, der jedoch zu stark vereinfacht wurde und somit einem Sonderfall erlag.
Stuart Marks
1
@necromancer Ich habe den letzten Absatz ein wenig bearbeitet, um die Frage zu klären, die Sie meiner Meinung nach stellen. Ist es richtig? Ist dies auch eine Frage zu einem gerichteten (nicht azyklischen) Graphen? (Nicht, dass es viel ausmacht.) Schließlich sollte die gewünschte Ausgabe sein [0, 2, 4]?
Stuart Marks
1
Ich glaube, dass die richtige Lösung, um dies zu beheben, darin besteht, ein zukünftiges Java-Release-Support-Tupel als Rückgabetyp (als Sonderfall von Object) zu haben und Lambda-Ausdrücke in der Lage zu sein, ein solches Tupel direkt für seine Parameter zu verwenden.
Thorbjørn Ravn Andersen

Antworten:

205

UPDATE: Diese Antwort ist eine Antwort auf die ursprüngliche Frage: Hat Java SE 8 Paare oder Tupel? (Und implizit, wenn nicht, warum nicht?) Das OP hat die Frage mit einem vollständigeren Beispiel aktualisiert, aber es scheint, dass sie ohne Verwendung einer Paarstruktur gelöst werden kann. [Anmerkung von OP: Hier ist die andere richtige Antwort .]


Die kurze Antwort lautet nein. Sie müssen entweder Ihre eigenen rollen oder eine der mehreren Bibliotheken einbinden, die sie implementieren.

Eine PairKlasse in Java SE zu haben, wurde mindestens einmal vorgeschlagen und abgelehnt. Siehe diesen Diskussionsthread auf einer der OpenJDK-Mailinglisten. Die Kompromisse sind nicht offensichtlich. Einerseits gibt es viele Paarimplementierungen in anderen Bibliotheken und im Anwendungscode. Dies zeigt einen Bedarf, und das Hinzufügen einer solchen Klasse zu Java SE erhöht die Wiederverwendung und Freigabe. Andererseits erhöht die Verwendung einer Pair-Klasse die Versuchung, komplizierte Datenstrukturen aus Paaren und Sammlungen zu erstellen, ohne die erforderlichen Typen und Abstraktionen zu erstellen. (Das ist eine Umschreibung von Kevin Bourillions Botschaft aus diesem Thread.)

Ich empfehle jedem, den gesamten E-Mail-Thread zu lesen. Es ist bemerkenswert aufschlussreich und hat keine Flammen. Es ist ziemlich überzeugend. Als es anfing, dachte ich: "Ja, es sollte eine Pair-Klasse in Java SE geben", aber als der Thread sein Ende erreichte, hatte ich meine Meinung geändert.

Beachten Sie jedoch, dass JavaFX das javafx.util.Pair hat Klasse . Die APIs von JavaFX wurden getrennt von den Java SE-APIs entwickelt.

Wie aus der verknüpften Frage hervorgeht, entspricht das C ++ - Paar in Java?Es gibt einen ziemlich großen Designraum, der eine scheinbar so einfache API umgibt. Sollten die Objekte unveränderlich sein? Sollten sie serialisierbar sein? Sollten sie vergleichbar sein? Sollte die Klasse endgültig sein oder nicht? Sollten die beiden Elemente bestellt werden? Sollte es eine Schnittstelle oder eine Klasse sein? Warum bei Paaren anhalten? Warum nicht Tripel, Quads oder N-Tupel?

Und natürlich gibt es die unvermeidliche Namensgebung für die Elemente:

  • (a, b)
  • (erste Sekunde)
  • (links rechts)
  • (Auto, CDR)
  • (foo, bar)
  • etc.

Ein großes Problem, das kaum erwähnt wurde, ist das Verhältnis von Paaren zu Primitiven. Wenn Sie ein (int x, int y)Datum haben, das einen Punkt im 2D-Raum darstellt, Pair<Integer, Integer>verbraucht dies drei Objekte anstelle von zwei 32-Bit-Wörtern. Darüber hinaus müssen sich diese Objekte auf dem Heap befinden und verursachen GC-Overhead.

Es scheint klar zu sein, dass es wie bei Streams wesentlich ist, dass es primitive Spezialisierungen für Paare gibt. Wollen wir sehen:

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

Sogar ein IntIntPairwürde noch ein Objekt auf dem Haufen benötigen.

Diese erinnern natürlich an die Verbreitung funktionaler Schnittstellen im java.util.functionPaket in Java SE 8. Wenn Sie keine aufgeblähte API möchten, welche würden Sie weglassen? Sie könnten auch argumentieren, dass dies nicht ausreicht und dass beispielsweise auch Spezialisierungen Booleanhinzugefügt werden sollten.

Ich habe das Gefühl, dass Java, wenn es vor langer Zeit eine Pair-Klasse hinzugefügt hätte, einfach oder sogar simpel gewesen wäre und viele der Anwendungsfälle, die wir uns jetzt vorstellen, nicht erfüllt hätte. Wenn Pair im Zeitrahmen von JDK 1.0 hinzugefügt worden wäre, wäre es wahrscheinlich veränderlich gewesen! (Schauen Sie sich java.util.Date an.) Wären die Leute damit zufrieden gewesen? Ich vermute, wenn es eine Pair-Klasse in Java gäbe, wäre sie irgendwie nicht wirklich nützlich, und jeder wird immer noch seine eigenen rollen, um seine Anforderungen zu erfüllen. Es würde verschiedene Pair- und Tuple-Implementierungen in externen Bibliotheken geben. und die Leute würden immer noch darüber streiten / diskutieren, wie Javas Pair-Klasse repariert werden kann. Mit anderen Worten, irgendwie an dem Ort, an dem wir uns heute befinden.

Inzwischen wird einige Arbeit auf die grundlegende Frage anzusprechen, die eine bessere Unterstützung in der JVM ist (und schließlich die Java - Sprache) für Werttypen . Siehe dieses Dokument zum Status der Werte . Dies ist eine vorläufige, spekulative Arbeit, die nur Themen aus der Sicht der JVM abdeckt, aber bereits einiges an Gedanken hinter sich hat. Natürlich gibt es keine Garantie dafür, dass dies in Java 9 oder jemals irgendwohin gelangt, aber es zeigt die aktuelle Denkrichtung zu diesem Thema.

Stuart Marks
quelle
3
@necromancer Factory-Methoden mit Grundelementen helfen nicht weiter Pair<T,U>. Da Generika müssen vom Referenztyp sein. Alle Grundelemente werden beim Speichern eingerahmt. Um Primitive zu speichern, benötigen Sie wirklich eine andere Klasse.
Stuart Marks
3
@necromancer Und ja, im Nachhinein sollten die primitiven Konstruktoren in Boxen nicht öffentlich gewesen sein und valueOfder einzige Weg gewesen sein, eine Instanz in Boxen zu erhalten. Aber diese sind seit Java 1.0 vorhanden und es lohnt sich wahrscheinlich nicht, sie an dieser Stelle zu ändern.
Stuart Marks
3
Offensichtlich sollte es nur eine öffentliche Pairoder TupleKlasse mit einer Factory-Methode geben, die die erforderlichen Spezialisierungsklassen (mit optimiertem Speicher) transparent im Hintergrund erstellt. Am Ende tun Lambdas genau das: Sie können eine beliebige Anzahl von Variablen beliebigen Typs erfassen. Und jetzt stellen Sie sich eine Sprachunterstützung vor, die es ermöglicht, zur Laufzeit die entsprechende Tupelklasse zu erstellen, die durch eine invokedynamicAnweisung ausgelöst wird …
Holger
3
@Holger So etwas könnte funktionieren, wenn man Werttypen in die vorhandene JVM nachrüsten würde, aber der Vorschlag für Werttypen (jetzt "Projekt Valhalla" ) ist viel radikaler. Insbesondere würden seine Werttypen nicht unbedingt Heap-zugewiesen. Im Gegensatz zu Objekten von heute und wie Primitiven von heute hätten Werte auch keine Identität.
Stuart Marks
2
@Stuart Marks: Das würde nicht stören, da der von mir beschriebene Typ der "Boxed" -Typ für einen solchen Werttyp sein könnte. Mit einer invokedynamicFabrik, die der Lambda-Kreation ähnelt, wäre eine solche spätere Nachrüstung kein Problem. Lambdas haben übrigens auch keine Identität. Wie bereits erwähnt, ist die Identität, die Sie heute möglicherweise wahrnehmen, ein Artefakt der aktuellen Implementierung.
Holger
45

Sie können sich diese integrierten Klassen ansehen:

senerh
quelle
3
Dies ist die richtige Antwort, was die integrierte Funktionalität für Paare betrifft. Beachten Sie, dass SimpleImmutableEntrynur garantiert wird, dass sich die in der gespeicherten Referenzen Entrynicht ändern, nicht, dass sich die Felder der verknüpften Objekte keyund der valueObjekte (oder der Objekte, mit denen sie verknüpft sind) nicht ändern.
Luke Hutchison
22

Leider hat Java 8 keine Paare oder Tupel eingeführt. Sie können natürlich immer org.apache.commons.lang3.tuple verwenden (was ich persönlich in Kombination mit Java 8 verwende) oder Sie können Ihre eigenen Wrapper erstellen. Oder verwenden Sie Karten. Oder ähnliches, wie in der akzeptierten Antwort auf die Frage, mit der Sie verlinkt haben, erklärt wird.


UPDATE: JDK 14 führt Datensätze als Vorschaufunktion ein. Dies sind keine Tupel, aber sie können verwendet werden, um viele der gleichen Probleme zu speichern. In Ihrem speziellen Beispiel von oben könnte das ungefähr so ​​aussehen:

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

Wenn Sie mit JDK 14 (zum Zeitpunkt des Schreibens ein Build mit frühem Zugriff) unter Verwendung des --enable-previewFlags kompiliert und ausgeführt werden , erhalten Sie das folgende Ergebnis:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]
blalasaadri
quelle
Eigentlich erlaubte mir eine der Antworten von @StuartMarks, es ohne Tupel zu lösen, aber da es nicht zu verallgemeinern scheint, werde ich es wahrscheinlich irgendwann brauchen.
Nekromant
@necromancer Ja, das ist eine sehr gute Antwort. Die Apache-Bibliothek kann manchmal immer noch nützlich sein, aber alles hängt vom Design der Javas-Sprache ab. Grundsätzlich müssten Tupel Primitive (oder ähnliche) sein, um wie in anderen Sprachen zu funktionieren.
Blalasaadri
1
Falls Sie es nicht bemerkt haben, enthielt die Antwort diesen äußerst informativen Link: cr.openjdk.java.net/~jrose/values/values-0.html über die Notwendigkeit und die Aussichten für solche Grundelemente, einschließlich Tupel.
Nekromant
17

Es scheint, dass das vollständige Beispiel ohne die Verwendung einer Paarstruktur gelöst werden kann. Der Schlüssel besteht darin, nach den Spaltenindizes zu filtern, wobei das Prädikat die gesamte Spalte überprüft, anstatt die Spaltenindizes der Anzahl der falseEinträge in dieser Spalte zuzuordnen.

Der Code, der dies tut, ist hier:

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

Dies führt zu einer Ausgabe, von [0, 2, 4]der ich denke, dass sie das vom OP angeforderte korrekte Ergebnis ist.

Beachten Sie auch die boxed()Operation, bei der die intWerte in IntegerObjekte eingeteilt werden. Dies ermöglicht es einem, den bereits vorhandenen toList()Kollektor zu verwenden, anstatt Kollektorfunktionen ausschreiben zu müssen, die das Boxen selbst ausführen.

Stuart Marks
quelle
1
+1 Ass im Ärmel :) Das verallgemeinert sich immer noch nicht, oder? Das war der wesentlichere Aspekt der Frage, da ich davon ausgehe, dass andere Situationen auftreten, in denen ein solches Schema nicht funktioniert (z. B. Spalten mit nicht mehr als 3 Werten true). Dementsprechend werde ich Ihre andere Antwort als richtig akzeptieren, aber auch auf diese hinweisen! Vielen Dank :)
Nekromant
Dies ist korrekt, akzeptiert jedoch die andere Antwort desselben Benutzers. (Siehe Kommentare oben und anderswo.)
Nekromant
1
@necromancer Richtig, diese Technik ist in den Fällen, in denen Sie den Index möchten, nicht ganz allgemein, aber das Datenelement kann nicht mithilfe des Index abgerufen oder berechnet werden. (Zumindest nicht einfach.) Stellen Sie sich beispielsweise ein Problem vor, bei dem Sie Textzeilen aus einer Netzwerkverbindung lesen und die Zeilennummer der N-ten Zeile ermitteln möchten, die einem bestimmten Muster entspricht. Am einfachsten ist es, jede Linie einem Paar oder einer zusammengesetzten Datenstruktur zuzuordnen, um die Linien zu nummerieren. Es gibt wahrscheinlich einen hackigen, nebenwirkenden Weg, dies ohne eine neue Datenstruktur zu tun.
Stuart Marks
@StuartMarks, Ein Paar ist <T, U>. ein Tripel <T, U, V>. usw. Ihr Beispiel ist eine Liste, kein Paar.
Pacerier
7

Vavr (früher Javaslang genannt) ( http://www.vavr.io ) bietet auch Tupel (bis zu einer Größe von 8). Hier ist das Javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Dies ist ein einfaches Beispiel:

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

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

Warum JDK selbst bis jetzt nicht mit einer einfachen Art von Tupeln geliefert wurde, ist mir ein Rätsel. Das Schreiben von Wrapper-Klassen scheint ein alltägliches Geschäft zu sein.

wumpz
quelle
Einige Versionen von vavr verwendeten hinterhältige Überwürfe unter der Haube. Achten Sie darauf, diese nicht zu verwenden.
Thorbjørn Ravn Andersen
7

Seit Java 9 können Sie Instanzen Map.Entryeinfacher als zuvor erstellen :

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

Map.entrygibt eine nicht veränderbare zurück Entryund verbietet Nullen.

ZhekaKozlov
quelle
6

Da Sie sich nur um die Indizes kümmern, müssen Sie Tupeln überhaupt nicht zuordnen. Warum nicht einfach einen Filter schreiben, der die Suchelemente in Ihrem Array verwendet?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));
dkatzel
quelle
+1 für eine großartige, praktische Lösung. Ich bin mir jedoch nicht sicher, ob dies auf meine Situation verallgemeinert wird, in der ich die Werte im laufenden Betrieb generiere. Ich habe meine Frage als Array gestellt, um einen einfachen Fall zum Nachdenken anzubieten, und Sie haben eine hervorragende Lösung gefunden.
Nekromant
5

Ja.

Map.Entrykann als verwendet werden Pair.

Leider hilft es bei Java 8-Streams nicht, da das Problem darin besteht, dass Lambdas zwar mehrere Argumente annehmen können, die Java-Sprache jedoch nur die Rückgabe eines einzelnen Werts (Objekt oder primitiver Typ) zulässt. Dies bedeutet, dass Sie immer dann, wenn Sie einen Stream haben, ein einzelnes Objekt aus der vorherigen Operation übergeben bekommen. Dies ist ein Mangel in der Java-Sprache, denn wenn mehrere Rückgabewerte unterstützt würden UND Streams diese unterstützen würden, könnten Streams viel schönere, nicht triviale Aufgaben ausführen.

Bis dahin nützt es nur wenig.

EDIT 2018-02-12: Während ich an einem Projekt arbeitete, schrieb ich eine Hilfsklasse, die hilft, den Sonderfall zu behandeln, dass eine Kennung früher im Stream vorhanden ist, die Sie zu einem späteren Zeitpunkt benötigen, aber der dazwischen liegende Stream-Teil weiß nichts davon. Bis ich dazu komme, es selbst zu veröffentlichen, ist es bei IdValue.java mit einem Unit-Test bei IdValueTest.java verfügbar

Thorbjørn Ravn Andersen
quelle
2

Eclipse Collections hat Pairund alle Kombinationen von Grundelement- / Objektpaaren (für alle acht Grundelemente).

Die TuplesFactory kann Instanzen von erstellen Pair, und die PrimitiveTuplesFactory kann verwendet werden, um alle Kombinationen von Primitiv / Objekt-Paaren zu erstellen.

Wir haben diese hinzugefügt, bevor Java 8 veröffentlicht wurde. Sie waren nützlich, um Schlüssel- / Wert-Iteratoren für unsere primitiven Karten zu implementieren, die wir auch in allen primitiven / Objekt-Kombinationen unterstützen.

Wenn Sie bereit sind, den zusätzlichen Bibliotheksaufwand hinzuzufügen, können Sie die von Stuart akzeptierte Lösung verwenden und die Ergebnisse in einem Grundelement sammeln IntList, um Boxen zu vermeiden. Wir haben in Eclipse Collections 9.0 neue Methoden hinzugefügt , damit Int/Long/DoubleSammlungen aus Int/Long/DoubleStreams erstellt werden können.

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

Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.

Donald Raab
quelle