Czy Java SE 8 ma pary lub krotki?


185

Bawię się leniwymi operacyjnymi funkcjami w Javie SE 8 i chcę mapindeksować ido pary / krotki (i, value[i]), a następnie filterbazować na drugim value[i]elemencie, a na koniec wyprowadzać tylko indeksy.

Czy nadal muszę cierpieć z tego powodu: Jaki jest odpowiednik pary C ++ <L, R> w Javie? w nowej, odważnej erze lambd i strumieni?

Aktualizacja: Przedstawiłem raczej uproszczony przykład, który ma fajne rozwiązanie oferowane przez @dkatzel w jednej z poniższych odpowiedzi. Jednak nie uogólnia. Dlatego dodam bardziej ogólny przykład:

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

}

Daje to niepoprawne dane wyjściowe, [0, 0, 0]które odpowiadają liczbie wszystkich trzech kolumn false. Potrzebuję wskaźników tych trzech kolumn. Prawidłowe wyjście powinno być [0, 2, 4]. Jak mogę uzyskać ten wynik?


2
Jest tam już AbstractMap.SimpleImmutableEntry<K,V>od lat ... Ale w każdym razie, zamiast mapowania ido (i, value[i])tylko do filtrowania przez value[i]i mapowanie z powrotem i: dlaczego nie tylko przez filtr value[i]w pierwszej kolejności, bez mapowania?
Holger,

@ Holger Muszę wiedzieć, które indeksy tablicy zawierają wartości pasujące do kryteriów. Nie mogę tego zrobić bez zachowania iw strumieniu. Potrzebuję również value[i]kryteriów. Właśnie dlatego potrzebuję(i, value[i])
nekromanta

1
@necromancer Racja, działa tylko wtedy, gdy tanie jest dostać się do wartości z indeksu, takiego jak tablica, kolekcja o swobodnym dostępie lub niedroga funkcja. Wydaje mi się, że problemem jest to, że chciałeś przedstawić uproszczony przypadek użycia, ale został on nadmiernie uproszczony i dlatego ulegał specjalnemu przypadkowi.
Stuart Marks

1
@necromancer Edytowałem trochę ostatni akapit, aby wyjaśnić pytanie, które, jak myślę, zadajesz. Czy to jest poprawne? Czy to pytanie dotyczy ukierunkowanego (nie acyklicznego) wykresu? (Nie to, że ma to duże znaczenie). Wreszcie, czy powinno być pożądane wyjście [0, 2, 4]?
Stuart Marks

1
Uważam, że właściwym rozwiązaniem tego problemu jest posiadanie w przyszłości krotek obsługujących wydanie Java jako typu zwracanego (jako szczególny przypadek Object), a wyrażenia lambda będą mogły używać takiej krotki bezpośrednio dla swoich parametrów.
Thorbjørn Ravn Andersen

Odpowiedzi:


206

AKTUALIZACJA: Ta odpowiedź jest odpowiedzią na pierwotne pytanie: Czy Java SE 8 ma pary czy krotki? (I domyślnie, jeśli nie, dlaczego nie?) OP zaktualizował pytanie bardziej kompletnym przykładem, ale wydaje się, że można je rozwiązać bez użycia jakiejkolwiek struktury par. [Uwaga od OP: oto inna poprawna odpowiedź .]


Krótka odpowiedź brzmi: nie. Musisz albo stworzyć własną, albo wprowadzić jedną z kilku bibliotek, które ją implementują.

Posiadanie Pairzajęć z Java SE zostało zaproponowane i odrzucone przynajmniej raz. Zobacz ten wątek dyskusyjny na jednej z list mailingowych OpenJDK. Kompromisy nie są oczywiste. Z jednej strony istnieje wiele implementacji Par w innych bibliotekach i kodzie aplikacji. To pokazuje potrzebę, a dodanie takiej klasy do Java SE zwiększy ponowne użycie i udostępnianie. Z drugiej strony posiadanie klasy Pair zwiększa pokusę tworzenia skomplikowanych struktur danych z par i kolekcji bez tworzenia niezbędnych typów i abstrakcji. (To parafraza przesłania Kevina Bourilliona z tego wątku.)

Polecam wszystkim przeczytanie tego całego wątku e-mail. Jest niezwykle wnikliwy i nie ma ognia. To całkiem przekonujące. Kiedy się zaczęło, pomyślałem: „Tak, powinna być klasa Pair w Java SE”, ale zanim wątek dobiegł końca, zmieniłem zdanie.

Należy jednak pamiętać, że JavaFX ma klasę javafx.util.Pair . Interfejsy API JavaFX ewoluowały niezależnie od interfejsów API Java SE.

Jak widać z połączonego pytania Co jest równoważne parze C ++ w Javie? wokół dość podobnego API jest dość duża przestrzeń projektowa. Czy obiekty powinny być niezmienne? Czy powinny być możliwe do serializacji? Czy powinny być porównywalne? Czy klasa powinna być ostateczna, czy nie? Czy należy zamówić dwa elementy? Czy powinien to być interfejs czy klasa? Po co zatrzymywać się w parach? Dlaczego nie trzy, czterokąty lub N-krotki?

I oczywiście istnieje nieunikniona nazwa dla elementów:

  • (a, b)
  • (pierwsza sekunda)
  • (Lewo prawo)
  • (samochód, cdr)
  • (foo, bar)
  • itp.

Jednym wielkim problemem, o którym prawie nie wspomniano, jest związek par z prymitywami. Jeśli masz układ (int x, int y)odniesienia reprezentujący punkt w przestrzeni 2D, reprezentowanie go jako Pair<Integer, Integer>pochłania trzy obiekty zamiast dwóch 32-bitowych słów. Co więcej, obiekty te muszą znajdować się na stercie i będą obciążone GC.

Wydaje się jasne, że podobnie jak w przypadku strumieni, niezbędne byłoby istnienie prymitywnych specjalizacji dla par. Czy chcemy zobaczyć:

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

Nawet i IntIntPairnadal wymagałoby jednego obiektu na stercie.

Przypominają one oczywiście rozpowszechnianie się funkcjonalnych interfejsów w java.util.functionpakiecie w Javie SE 8. Jeśli nie chcesz rozdętego API, które byś pominął? Można również argumentować, że to nie wystarczy i że Booleannależy dodać także specjalizacje, powiedzmy, do .

Mam wrażenie, że gdyby Java dodała klasę Pair już dawno temu, byłoby to proste, a nawet uproszczone i nie spełniłoby wielu przypadków użycia, które teraz przewidujemy. Weź pod uwagę, że gdyby Para została dodana w ramach czasowych JDK 1.0, prawdopodobnie byłaby zmienna! (Spójrz na java.util.Date.) Czy ludzie byliby z tego zadowoleni? Domyślam się, że gdyby w Javie istniała klasa Pair, byłaby to trochę nieprzydatna i wszyscy nadal będą się rozwijać, aby zaspokoić swoje potrzeby, w bibliotekach zewnętrznych będą różne implementacje Pair i Tuple, a ludzie nadal będą się kłócić / dyskutować o tym, jak naprawić klasę Java pary. Innymi słowy, jakby w tym samym miejscu, w którym jesteśmy dzisiaj.

Tymczasem trwają prace nad fundamentalnym problemem, którym jest lepsza obsługa JVM (i ostatecznie języka Java) dla typów wartości . Zobacz ten dokument Stan wartości . Jest to wstępna, spekulacyjna praca, która obejmuje tylko kwestie z perspektywy JVM, ale ma już za sobą wiele przemyśleń. Oczywiście nie ma gwarancji, że wejdzie to w Javę 9 lub kiedykolwiek wejdzie gdziekolwiek, ale pokazuje aktualny kierunek myślenia na ten temat.


3
@necromancer Metody fabryczne z prymitywami nie pomagają Pair<T,U>. Ponieważ leki generyczne muszą być typu odniesienia. Wszelkie prymitywy zostaną zapakowane, gdy zostaną zapisane. Do przechowywania prymitywów naprawdę potrzebujesz innej klasy.
Stuart Marks

3
@necromancer I tak z perspektywy czasu prymitywne konstruktory w pudełkach nie powinny być publiczne i valueOfpowinny być jedynym sposobem na uzyskanie wystąpienia w pudełku. Ale są tam od wersji Java 1.0 i prawdopodobnie nie warto w tym momencie próbować ich zmieniać.
Stuart Marks

3
Oczywiście powinna istnieć tylko jedna klasa publiczna Pairlub Tupleklasa z fabryczną metodą tworzącą niezbędne klasy specjalizacji (ze zoptymalizowanym przechowywaniem) w sposób przezroczysty w tle. Ostatecznie lambdas robią dokładnie to: mogą przechwycić dowolną liczbę zmiennych dowolnego typu. A teraz wyobraź sobie obsługę języka pozwalającą na stworzenie odpowiedniej klasy krotek w czasie wykonywania wyzwalanym przez invokedynamicinstrukcję…
Holger

3
@Holger Coś w tym stylu mogłoby działać, gdyby ktoś przebudowywał typy wartości na istniejącą maszynę JVM, ale propozycja typów wartości (obecnie „Projekt Valhalla” ) jest znacznie bardziej radykalna. W szczególności jego typy wartości niekoniecznie byłyby alokowane na stercie. Ponadto, w przeciwieństwie do dzisiejszych obiektów i dzisiejszych prymitywów, wartości nie miałyby żadnej tożsamości.
Stuart Marks

2
@Stuart Marks: To by nie przeszkadzało, ponieważ opisany typ może być typem „pudełkowym” dla takiego typu wartości. Dzięki invokedynamicfabryce opartej na zasadzie podobnej do lambda takie późniejsze modernizacje nie stanowiłyby problemu. Nawiasem mówiąc, jagnięta również nie mają tożsamości. Jak wyraźnie powiedziano, tożsamość, którą możesz dziś postrzegać, jest artefaktem obecnej implementacji.
Holger

46

Możesz spojrzeć na te wbudowane klasy:


3
To poprawna odpowiedź, jeśli chodzi o wbudowaną funkcjonalność dla par. Zauważ, że SimpleImmutableEntrygwarantuje tylko, że odniesienia przechowywane w Entrynie zmieniają się, a nie, że pola połączonego obiektu keyi valueobiekty (lub te obiektów, do których się łączą) się nie zmieniają.
Luke Hutchison

22

Niestety Java 8 nie wprowadziła par ani krotek. Zawsze możesz oczywiście użyć org.apache.commons.lang3.tuple (którego osobiście używam w połączeniu z Javą 8) lub możesz stworzyć własne opakowania. Lub użyj Map. Lub coś w tym rodzaju, jak wyjaśniono w zaakceptowanej odpowiedzi na pytanie, z którym się łączysz.


AKTUALIZACJA: JDK 14 wprowadza rekordy jako funkcję podglądu. Nie są to krotki, ale można je wykorzystać do zapisania wielu takich samych problemów. W twoim konkretnym przykładzie z góry może to wyglądać mniej więcej tak:

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

Po skompilowaniu i uruchomieniu z JDK 14 (w chwili pisania tej wersji kompilacji z wczesnym dostępem) przy użyciu --enable-previewflagi otrzymujesz następujący wynik:

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

Właściwie jedna z odpowiedzi @StuartMarks pozwoliła mi rozwiązać ją bez krotek, ale ponieważ wydaje się, że nie uogólnia, prawdopodobnie w końcu będę jej potrzebować.
nekromanta

@necromancer Tak, to bardzo dobra odpowiedź. Biblioteka apache czasami może się przydać, ale wszystko sprowadza się do projektowania języka Java. Zasadniczo krotki musiałyby być prymitywami (lub podobnymi), aby działały tak jak w innych językach.
blalasaadri

1
Jeśli tego nie zauważyłeś, odpowiedź zawierała ten niezwykle pouczający link: cr.openjdk.java.net/~jrose/values/values-0.html o potrzebie i perspektywach takich prymitywów, w tym krotek.
nekromanta

17

Wygląda na to, że pełny przykład można rozwiązać bez użycia jakiejkolwiek struktury par. Kluczem jest filtrowanie według indeksów kolumn, przy czym predykat sprawdza całą kolumnę, zamiast mapować indeksy kolumn na liczbę falsewpisów w tej kolumnie.

Kod, który to robi, znajduje się tutaj:

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

Wynikiem tego [0, 2, 4]jest, moim zdaniem, poprawny wynik wymagany przez PO.

Zwróć także uwagę na boxed()operację dzielenia intwartości na Integerobiekty. Umożliwia to korzystanie z istniejącego toList()kolektora zamiast wypisywania funkcji kolektora, które same wykonują boks.


1
+1 asa w rękawie :) To wciąż się nie generalizuje, prawda? To był bardziej istotny aspekt pytania, ponieważ spodziewam się, że zmierzę się z innymi sytuacjami, w których taki schemat nie zadziała (na przykład kolumny z nie więcej niż 3 wartościami true). W związku z tym zaakceptuję Twoją drugą odpowiedź jako poprawną, ale również wskaż tę! Dziękuję bardzo :)
nekromanta

Jest to poprawne, ale akceptuje inną odpowiedź tego samego użytkownika. (patrz komentarze powyżej i gdzie indziej.)
nekromanta,

1
@necromancer Racja, ta technika nie jest w pełni ogólna w przypadkach, w których potrzebny jest indeks, ale elementu danych nie można odzyskać ani obliczyć za pomocą indeksu. (Przynajmniej nie łatwo.) Weźmy na przykład problem polegający na tym, że czytasz wiersze tekstu z połączenia sieciowego i chcesz znaleźć numer linii N-tej, która pasuje do jakiegoś wzorca. Najprostszym sposobem jest zamapowanie każdej linii na parę lub jakąś złożoną strukturę danych w celu numeracji linii. Prawdopodobnie jest to hackerski, efektywny z boku sposób, aby to zrobić bez nowej struktury danych.
Stuart Marks

@StuartMarks, para to <T, U>. potrójne <T, U, V>. itp. Twój przykład to lista, a nie para.
Pacerier

7

Vavr (wcześniej nazywany JavaSlang) ( http://www.vavr.io ) zapewnia również krotki (do rozmiaru 8). Oto javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

To jest prosty przykład:

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

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

Dlaczego sam JDK nie przyszedł z prostymi krotkami do teraz, jest dla mnie tajemnicą. Pisanie klas opakowań wydaje się być codziennym zajęciem.


Niektóre wersje vavr używały podstępnych rzutów pod maską. Uważaj, aby ich nie używać.
Thorbjørn Ravn Andersen

7

Od wersji Java 9 możesz tworzyć instancje Map.Entryłatwiejsze niż wcześniej:

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

Map.entryzwraca niemodyfikowalne Entryi zabrania zerowania.


6

Ponieważ zależy Ci tylko na indeksach, nie musisz w ogóle mapować krotek. Dlaczego po prostu nie napisać filtru, który korzysta z elementów wyszukiwania w tablicy?

     int[] value =  ...


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

+1 za świetne, praktyczne rozwiązanie. Nie jestem jednak pewien, czy uogólnia to na moją sytuację, w której generuję wartości w locie. Zadałem to pytanie jako tablicę, aby zaoferować prosty przypadek do przemyślenia, a ty znalazłeś doskonałe rozwiązanie.
nekromanta

5

Tak.

Map.Entrymoże być używany jako Pair.

Niestety nie pomaga w strumieniach Java 8, ponieważ problem polega na tym, że chociaż lambdas mogą przyjmować wiele argumentów, język Java pozwala na zwrócenie tylko jednej wartości (typu obiektowego lub pierwotnego). Oznacza to, że ilekroć masz strumień, kończysz się przekazaniem jednego obiektu z poprzedniej operacji. Jest to brak w języku Java, ponieważ jeśli obsługiwanych jest wiele wartości zwracanych ORAZ obsługiwane są strumienie, moglibyśmy wykonywać o wiele ładniejsze nietrywialne zadania wykonywane przez strumienie.

Do tego czasu wykorzystanie jest niewielkie.

EDYCJA 2018-02-12: Podczas pracy nad projektem napisałem klasę pomocnika, która pomaga w rozwiązaniu szczególnego przypadku posiadania identyfikatora wcześniej w strumieniu, którego potrzebujesz później, ale część strumienia pomiędzy nie wie o tym. Dopóki się nie obejrzę i nie wydam go samodzielnie, jest on dostępny na IdValue.java z testem jednostkowym na IdValueTest.java


2

Kolekcje Eclipse mają Pairi wszystkie kombinacje par pierwotnych / obiektowych (dla wszystkich ośmiu pierwotnych).

TuplesFabryka może tworzyć instancje Pairi PrimitiveTuplesfabryczne mogą być wykorzystywane do tworzenia wszystkich kombinacji par pierwotnych / obiektów.

Dodaliśmy je przed wydaniem Java 8. Przydały się one do implementacji iteratorów klucz / wartość dla naszych prymitywnych map, które obsługujemy również we wszystkich kombinacjach prymitywów / obiektów.

Jeśli chcesz dodać dodatkowy narzut biblioteki, możesz użyć zaakceptowanego rozwiązania Stuarta i zebrać wyniki w prymitywne, IntListaby uniknąć boksu. Dodaliśmy nowe metody w Eclipse Collections 9.0, aby umożliwić tworzenie Int/Long/Doublekolekcji ze Int/Long/Doublestrumieni.

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

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.