Apakah Java SE 8 memiliki Pairs atau Tuples?

185

Saya bermain-main dengan operasi fungsional yang malas di Java SE 8, dan saya ingin mapindeks iuntuk pasangan / tuple (i, value[i]), kemudian filterberdasarkan pada value[i]elemen kedua , dan akhirnya hanya output indeks.

Haruskah saya tetap menderita ini: Apa yang setara dengan C ++ Pair <L, R> di Jawa? di era baru lambda dan aliran yang berani?

Pembaruan: Saya menyajikan contoh yang agak disederhanakan, yang memiliki solusi rapi yang ditawarkan oleh @dkatzel di salah satu jawaban di bawah ini. Namun, itu tidak menyamaratakan. Karena itu, izinkan saya menambahkan contoh yang lebih umum:

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

}

Ini memberikan output yang salah[0, 0, 0] yang sesuai dengan jumlah untuk tiga kolom yang semuanya false. Yang saya butuhkan adalah indeks dari tiga kolom ini. Output yang benar seharusnya [0, 2, 4]. Bagaimana saya bisa mendapatkan hasil ini?

ahli nujum
sumber
2
Sudah ada AbstractMap.SimpleImmutableEntry<K,V>selama bertahun-tahun ... Tapi bagaimanapun, bukannya pemetaan iuntuk (i, value[i])hanya untuk menyaring oleh value[i]dan pemetaan kembali ke i: mengapa tidak hanya filter by value[i]di tempat pertama, tanpa pemetaan?
Holger
@ Holger Saya perlu tahu indeks array mana yang mengandung nilai yang cocok dengan kriteria. Saya tidak bisa melakukannya tanpa melestarikan idalam arus. Saya juga perlu value[i]untuk kriteria. Itu sebabnya saya perlu(i, value[i])
ahli nujum
1
@necromancer Benar, itu hanya berfungsi jika murah untuk mendapatkan nilai dari indeks, seperti array, kumpulan akses acak, atau fungsi yang tidak mahal. Saya kira masalahnya adalah Anda ingin menyajikan kasus penggunaan yang disederhanakan, tetapi terlalu disederhanakan dan dengan demikian menyerah pada kasus khusus.
Stuart Marks
1
@necromancer Saya sedikit mengedit paragraf terakhir untuk mengklarifikasi pertanyaan yang menurut Anda Anda tanyakan. Apakah tepat? Juga, apakah ini pertanyaan tentang grafik yang diarahkan (bukan asiklik)? (Bukan berarti itu penting.) Akhirnya, haruskah output yang diinginkan [0, 2, 4]?
Stuart Marks
1
Saya percaya bahwa solusi yang tepat untuk memperbaikinya adalah memiliki tuple dukungan rilis Java yang akan datang sebagai tipe pengembalian (sebagai kasus khusus Object) dan membuat ekspresi lambda dapat menggunakan tuple tersebut secara langsung untuk parameternya.
Thorbjørn Ravn Andersen

Jawaban:

206

UPDATE: Jawaban ini sebagai tanggapan terhadap pertanyaan awal, Apakah Java SE 8 memiliki Pasangan atau Tuple? (Dan secara implisit, jika tidak, mengapa tidak?) OP telah memperbarui pertanyaan dengan contoh yang lebih lengkap, tetapi sepertinya itu dapat diselesaikan tanpa menggunakan struktur Pair. [Catatan dari OP: inilah jawaban yang benar .]


Jawaban singkatnya adalah tidak. Anda harus menggulung sendiri atau membawa salah satu dari beberapa perpustakaan yang mengimplementasikannya.

Memiliki Pairkelas di Jawa SE diusulkan dan ditolak setidaknya sekali. Lihat utas diskusi ini di salah satu milis OpenJDK. Pengorbanannya tidak jelas. Di satu sisi, ada banyak implementasi Pair di perpustakaan lain dan dalam kode aplikasi. Itu menunjukkan kebutuhan, dan menambahkan kelas seperti itu ke Java SE akan meningkatkan penggunaan kembali dan berbagi. Di sisi lain, memiliki kelas Pair menambah godaan untuk menciptakan struktur data yang rumit dari Pasangan dan koleksi tanpa membuat jenis dan abstraksi yang diperlukan. (Itu adalah parafrase dari pesan Kevin Bourillion dari utas itu.)

Saya sarankan semua orang membaca seluruh utas email itu. Ini sangat berwawasan dan tidak memiliki kerusakan. Cukup meyakinkan. Ketika mulai, saya berpikir, "Ya, seharusnya ada kelas Pair di Java SE" tetapi pada saat thread mencapai akhirnya saya telah berubah pikiran.

Perhatikan bahwa JavaFX memiliki kelas javafx.util.Pair . API JavaFX berevolusi secara terpisah dari Java SE API.

Seperti yang dapat dilihat dari pertanyaan terkait Apa persamaan dari C ++ Pair in Java? ada ruang desain yang cukup besar di sekitar apa yang tampaknya seperti API sederhana. Haruskah benda tidak berubah? Haruskah mereka serial? Haruskah mereka sebanding? Haruskah kelasnya final atau tidak? Haruskah kedua elemen dipesan? Haruskah itu antarmuka atau kelas? Mengapa berhenti berpasangan? Mengapa tidak tiga kali lipat, paha depan, atau N-tupel?

Dan tentu saja ada penamaan yang tak terhindarkan bikeshed untuk elemen:

  • (a, b)
  • (pertama kedua)
  • (kiri kanan)
  • (mobil, cdr)
  • (foo, bar)
  • dll.

Satu masalah besar yang hampir tidak disebutkan adalah hubungan Pasangan dengan primitif. Jika Anda memiliki (int x, int y)datum yang mewakili titik dalam ruang 2D, mewakili ini sebagai Pair<Integer, Integer>mengkonsumsi tiga objek, bukan dua kata 32-bit. Selanjutnya, benda-benda ini harus berada di heap dan akan dikenakan overhead GC.

Tampak jelas bahwa, seperti Streams, penting untuk ada spesialisasi primitif untuk Pasangan. Apakah kita ingin melihat:

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

Bahkan sebuah IntIntPairmasih akan membutuhkan satu objek di heap.

Ini, tentu saja, mengingatkan pada proliferasi antarmuka fungsional dalam java.util.functionpaket di Java SE 8. Jika Anda tidak ingin API kembung, yang mana yang akan Anda tinggalkan? Anda juga bisa berpendapat bahwa ini tidak cukup, dan bahwa spesialisasi untuk, katakanlah, Booleanharus ditambahkan juga.

Perasaan saya adalah bahwa jika Java telah menambahkan kelas Pair sejak lama, itu akan menjadi sederhana, atau bahkan sederhana, dan itu tidak akan memuaskan banyak kasus penggunaan yang kita bayangkan sekarang. Pertimbangkan bahwa jika Pair telah ditambahkan dalam kerangka waktu JDK 1.0, itu mungkin akan bisa berubah! (Lihatlah java.util.Date.) Apakah orang akan senang dengan itu? Dugaan saya adalah bahwa jika ada kelas Pair di Jawa, itu akan menjadi semacam-agak-tidak-benar-berguna dan semua orang masih akan bergulir sendiri untuk memenuhi kebutuhan mereka, akan ada berbagai implementasi Pair dan Tuple di perpustakaan eksternal, dan orang-orang masih akan berdebat / berdiskusi tentang cara memperbaiki kelas Pasangan Jawa. Dengan kata lain, jenis di tempat yang sama kita di hari ini.

Sementara itu, beberapa pekerjaan sedang berlangsung untuk mengatasi masalah mendasar, yang merupakan dukungan yang lebih baik dalam JVM (dan akhirnya bahasa Jawa) untuk tipe nilai . Lihat dokumen Status Nilai ini . Ini adalah pekerjaan pendahuluan dan spekulatif, dan hanya membahas masalah dari perspektif JVM, tetapi sudah ada cukup banyak pemikiran di baliknya. Tentu saja tidak ada jaminan bahwa ini akan masuk ke Java 9, atau pernah masuk ke mana saja, tetapi hal itu menunjukkan arah pemikiran saat ini tentang topik ini.

Stuart Marks
sumber
3
@necromancer Metode pabrik dengan primitif tidak membantu Pair<T,U>. Karena obat generik harus dari jenis referensi. Primitif apa pun akan dikotak ketika disimpan. Untuk menyimpan primitif Anda benar-benar membutuhkan kelas yang berbeda.
Stuart Marks
3
@necromancer Dan ya kalau dipikir-pikir konstruktor primitif kotak seharusnya tidak publik, dan valueOfseharusnya menjadi satu-satunya cara untuk mendapatkan contoh kotak. Tetapi itu sudah ada di sana sejak Java 1.0 dan mungkin tidak layak untuk dicoba pada titik ini.
Stuart Marks
3
Jelas, seharusnya hanya ada satu publik Pairatau Tuplekelas dengan metode pabrik yang menciptakan kelas spesialisasi yang diperlukan (dengan penyimpanan yang dioptimalkan) secara transparan di latar belakang. Pada akhirnya, lambdas melakukan hal itu: mereka dapat menangkap sejumlah variabel tipe arbitrer. Dan sekarang gambar dukungan bahasa yang memungkinkan untuk membuat kelas tuple yang sesuai saat runtime dipicu oleh invokedynamicinstruksi ...
Holger
3
@ Holger Sesuatu seperti itu mungkin bekerja jika seseorang memperbaiki tipe nilai ke JVM yang ada, tetapi proposal Value Value (sekarang "Project Valhalla" ) jauh lebih radikal. Khususnya, tipe nilainya tidak perlu dialokasikan heap. Juga, tidak seperti objek hari ini, dan seperti primitif saat ini, nilai tidak akan memiliki identitas.
Stuart Marks
2
@Stuart Marks: Itu tidak akan mengganggu karena tipe yang saya jelaskan bisa menjadi tipe “kotak” untuk tipe nilai seperti itu. Dengan invokedynamicpabrik berbasis yang mirip dengan pembuatan lambda perkuatan seperti itu di kemudian hari tidak akan menjadi masalah. Omong-omong, lambda tidak memiliki identitas juga. Seperti yang dinyatakan secara eksplisit, identitas yang mungkin Anda rasakan hari ini adalah artefak dari implementasi saat ini.
Holger
46

Anda dapat melihat kelas bawaan ini:

senerh
sumber
3
Ini adalah jawaban yang benar, sejauh fungsi bawaan untuk berpasangan. Perhatikan bahwa SimpleImmutableEntryhanya menjamin bahwa referensi yang disimpan di Entrytidak berubah, bukan bidang yang ditautkan keydan valueobjek (atau objek yang ditautkan) tidak berubah.
Luke Hutchison
22

Sayangnya, Java 8 tidak memperkenalkan pasangan atau tupel. Anda selalu dapat menggunakan org.apache.commons.lang3.tuple tentu saja (yang secara pribadi saya gunakan dalam kombinasi dengan Java 8) atau Anda dapat membuat pembungkus sendiri. Atau gunakan Maps. Atau hal-hal seperti itu, seperti dijelaskan dalam jawaban yang diterima untuk pertanyaan yang Anda tautkan.


PEMBARUAN: JDK 14 memperkenalkan catatan sebagai fitur pratinjau. Ini bukan tupel, tetapi dapat digunakan untuk menyimpan banyak masalah yang sama. Dalam contoh spesifik Anda dari atas, itu bisa terlihat seperti ini:

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

Ketika dikompilasi dan dijalankan dengan JDK 14 (pada saat penulisan, ini merupakan akses awal) menggunakan --enable-previewflag, Anda mendapatkan hasil berikut:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]
blalasaadri
sumber
Sebenarnya salah satu jawaban oleh @StuartMarks memungkinkan saya untuk menyelesaikannya tanpa tupel, tetapi karena tampaknya tidak menggeneralisasi saya mungkin akan membutuhkannya pada akhirnya.
ahli nujum
@necromancer Ya, itu adalah jawaban yang sangat bagus. Pustaka apache kadang-kadang masih bisa berguna, tetapi semuanya tergantung pada desain bahasa Jawa. Pada dasarnya, tuple harus primitif (atau serupa) untuk bekerja seperti yang mereka lakukan dalam bahasa lain.
blalasaadri
1
Jika Anda tidak menyadarinya, jawabannya menyertakan tautan yang sangat informatif ini: cr.openjdk.java.net/~jrose/values/values-0.html tentang kebutuhan dan prospek primitif seperti termasuk tupel.
ahli nujum
17

Tampaknya contoh lengkap dapat diselesaikan tanpa menggunakan segala jenis struktur Pair. Kuncinya adalah menyaring indeks kolom, dengan predikat memeriksa seluruh kolom, alih-alih memetakan indeks kolom ke jumlah falseentri dalam kolom itu.

Kode yang melakukan ini ada di sini:

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

Ini menghasilkan output [0, 2, 4]yang saya pikir hasil yang benar diminta oleh OP.

Perhatikan juga boxed()operasi yang mengotakkan intnilai ke dalam Integerobjek. Ini memungkinkan seseorang untuk menggunakan toList()kolektor yang sudah ada alih-alih harus menulis fungsi kolektor yang melakukan tinju sendiri.

Stuart Marks
sumber
1
+1 ace up your sleeve :) Ini masih belum menggeneralisasi, kan? Itu adalah aspek yang lebih substansial dari pertanyaan karena saya berharap untuk menghadapi situasi lain di mana skema seperti ini tidak akan berfungsi (misalnya kolom dengan nilai tidak lebih dari 3 true). Oleh karena itu, saya akan menerima jawaban Anda yang lain sebagai benar, tetapi juga tunjukkan yang ini! Terima kasih banyak :)
necromancer
Ini benar tetapi menerima jawaban lain oleh pengguna yang sama. (lihat komentar di atas dan di tempat lain.)
necromancer
1
@necromancer Benar, teknik ini tidak sepenuhnya umum dalam kasus di mana Anda menginginkan indeks, tetapi elemen data tidak dapat diambil atau dihitung menggunakan indeks. (Setidaknya tidak mudah.) Misalnya, pertimbangkan masalah ketika Anda membaca baris teks dari koneksi jaringan, dan Anda ingin menemukan nomor baris dari baris ke-N yang cocok dengan beberapa pola. Cara termudah adalah memetakan setiap baris menjadi pasangan atau beberapa struktur data komposit untuk memberi nomor pada garis. Mungkin ada cara yang efektif dan efek samping untuk melakukan ini tanpa struktur data baru.
Stuart Marks
@StuartMarks, Sepasang adalah <T, U>. triple <T, U, V>. dll. Contoh Anda adalah daftar, bukan pasangan.
Pacerier
7

Vavr (sebelumnya disebut Javaslang) ( http://www.vavr.io ) juga menyediakan tupel (hingga ukuran 8). Inilah javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Ini adalah contoh sederhana:

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

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

Mengapa JDK sendiri tidak datang dengan jenis tuple yang sederhana sampai sekarang adalah misteri bagi saya. Menulis kelas bungkus tampaknya menjadi bisnis setiap hari.

wumpz
sumber
Beberapa versi vavr menggunakan lemparan licik di bawah tenda. Berhati-hatilah untuk tidak menggunakannya.
Thorbjørn Ravn Andersen
7

Sejak Java 9, Anda dapat membuat instance yang Map.Entrylebih mudah daripada sebelumnya:

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

Map.entrymengembalikan yang tidak dapat dimodifikasi Entrydan melarang nulls.

ZhekaKozlov
sumber
6

Karena Anda hanya peduli pada indeks, Anda tidak perlu memetakan ke tuple sama sekali. Mengapa tidak menulis filter yang menggunakan elemen pencarian di array Anda?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));
dkatzel
sumber
+1 untuk solusi praktis yang bagus. Akan tetapi, saya tidak yakin apakah itu berlaku untuk situasi saya di mana saya menghasilkan nilai-nilai dengan cepat. Saya mengajukan pertanyaan saya sebagai array untuk menawarkan kasus sederhana untuk dipikirkan dan Anda memang menghasilkan solusi yang sangat baik.
ahli nujum
5

Iya.

Map.Entrydapat digunakan sebagai Pair.

Sayangnya itu tidak membantu dengan stream Java 8 karena masalahnya adalah bahwa meskipun lambdas dapat mengambil beberapa argumen, bahasa Java hanya memungkinkan untuk mengembalikan nilai tunggal (objek atau tipe primitif). Ini menyiratkan bahwa setiap kali Anda memiliki aliran Anda berakhir dengan dilewatkan satu objek dari operasi sebelumnya. Ini adalah kekurangan dalam bahasa Java, karena jika beberapa nilai balik didukung DAN stream mendukung mereka, kita bisa memiliki tugas non-sepele yang jauh lebih baik dilakukan oleh stream.

Sampai saat itu, hanya ada sedikit kegunaan.

EDIT 2018-02-12: Saat mengerjakan proyek saya menulis kelas pembantu yang membantu menangani kasus khusus memiliki pengidentifikasi sebelumnya dalam aliran yang Anda butuhkan di lain waktu tetapi bagian aliran di antara tidak tahu tentang itu. Sampai saya sempat merilis sendiri, itu tersedia di IdValue.java dengan unit test di IdValueTest.java

Thorbjørn Ravn Andersen
sumber
2

Eclipse Collections memiliki Pairdan semua kombinasi Pasangan primitif / objek (untuk semua delapan primitif).

The Tuplespabrik dapat membuat contoh Pair, dan PrimitiveTuplespabrik dapat digunakan untuk membuat semua kombinasi dari primitif pasang / objek.

Kami menambahkan ini sebelum Java 8 dirilis. Mereka berguna untuk mengimplementasikan key / value Iterators untuk peta primitif kami, yang kami juga dukung di semua kombinasi primitif / objek.

Jika Anda ingin menambahkan overhead perpustakaan tambahan, Anda dapat menggunakan solusi yang diterima Stuart dan mengumpulkan hasilnya menjadi primitif IntListuntuk menghindari tinju. Kami menambahkan metode baru di Eclipse Collections 9.0 untuk memungkinkan Int/Long/Doublekoleksi dibuat dari Int/Long/DoubleStreams.

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

Catatan: Saya pengendara untuk Eclipse Collections.

Donald Raab
sumber