Pengantar Spliterator di Java

1. Ikhtisar

The Spliterator interface, diperkenalkan di Jawa 8, dapat digunakan untuk melintasi dan partisi urutan . Ini adalah utilitas dasar untuk Stream , terutama yang paralel.

Dalam artikel ini, kami akan membahas penggunaan, karakteristik, metode, dan cara membuat implementasi kustom kami sendiri.

2. API Spliterator

2.1. tryAdvance

Ini adalah metode utama yang digunakan untuk melangkah melalui suatu urutan. Metode ini mengambil Konsumen yang terbiasa menggunakan elemen Spliterator satu per satu secara berurutan dan mengembalikan false jika tidak ada elemen yang akan dilalui.

Di sini, kita akan melihat bagaimana menggunakannya untuk melintasi dan mempartisi elemen.

Pertama, mari kita asumsikan bahwa kita memiliki ArrayList dengan 35000 artikel dan kelas Artikel didefinisikan sebagai:

public class Article { private List listOfAuthors; private int id; private String name; // standard constructors/getters/setters }

Sekarang, mari kita terapkan tugas yang memproses daftar artikel dan menambahkan akhiran " - diterbitkan oleh Baeldung" ke setiap nama artikel:

public String call() { int current = 0; while (spliterator.tryAdvance(a -> a.setName(article.getName() .concat("- published by Baeldung")))) { current++; } return Thread.currentThread().getName() + ":" + current; }

Perhatikan bahwa tugas ini mengeluarkan jumlah artikel yang diproses ketika menyelesaikan eksekusi.

Poin penting lainnya adalah kami menggunakan metode tryAdvance () untuk memproses elemen berikutnya.

2.2. trySplit

Selanjutnya, mari kita pisahkan Spliterator (karena itu namanya) dan proses partisi secara mandiri.

The trySplit metode mencoba untuk membaginya menjadi dua bagian. Kemudian elemen proses pemanggil, dan akhirnya, instance yang dikembalikan akan memproses elemen lainnya, memungkinkan keduanya diproses secara paralel.

Mari buat daftar kita terlebih dahulu:

public static List generateElements() { return Stream.generate(() -> new Article("Java")) .limit(35000) .collect(Collectors.toList()); }

Selanjutnya, kami mendapatkan instance Spliterator kami menggunakan metode spliterator () . Kemudian kami menerapkan metode trySplit () kami :

@Test public void givenSpliterator_whenAppliedToAListOfArticle_thenSplittedInHalf() { Spliterator split1 = Executor.generateElements().spliterator(); Spliterator split2 = split1.trySplit(); assertThat(new Task(split1).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); assertThat(new Task(split2).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); }

Proses pemisahan bekerja sebagaimana mestinya dan membagi catatan secara merata .

2.3. estimSize

The estimatedSize metode memberi kita perkiraan jumlah elemen:

LOG.info("Size: " + split1.estimateSize());

Ini akan menghasilkan:

Size: 17500

2.4. hasCharacteristics

API ini memeriksa apakah karakteristik yang diberikan cocok dengan properti Spliterator. Kemudian jika kita memanggil metode di atas, hasilnya adalah representasi int dari karakteristik tersebut:

LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464

3. Karakteristik Spliterator

Ia memiliki delapan karakteristik berbeda yang menggambarkan perilakunya. Itu dapat digunakan sebagai petunjuk untuk alat eksternal:

  • Berukuran - jika itu mampu kembali jumlah yang tepat dari elemen dengan estimateSize () metode
  • DIURUTKAN - jika melakukan iterasi melalui sumber yang diurutkan
  • SUBSIZED - jika kita membagi contoh menggunakan trySplit () metode dan memperoleh Spliterators yang berukuran juga
  • CONCURRENT - jika sumber dapat diubah dengan aman secara bersamaan
  • DISTINCT - jika untuk setiap pasangan elemen yang ditemukan x, y,! X.equals (y)
  • TIDAK DAPAT DIMUTUSKAN - jika elemen yang dipegang oleh sumber tidak dapat diubah secara struktural
  • NONNULL - jika sumber memiliki null atau tidak
  • ORDERED - jika melakukan iterasi pada urutan yang dipesan

4. Sebuah Spliterator Kustom

4.1. Kapan Menyesuaikan

Pertama, mari kita asumsikan skenario berikut:

Kami memiliki kelas artikel dengan daftar penulis, dan artikel yang dapat memiliki lebih dari satu penulis. Selanjutnya, kami mempertimbangkan seorang penulis yang terkait dengan artikel tersebut jika id artikel terkaitnya cocok dengan id artikel.

Kelas Penulis kami akan terlihat seperti ini:

public class Author { private String name; private int relatedArticleId; // standard getters, setters & constructors }

Selanjutnya, kami akan menerapkan kelas untuk menghitung penulis saat melintasi aliran penulis. Kemudian kelas akan melakukan pengurangan pada aliran tersebut.

Mari kita lihat implementasi kelas:

public class RelatedAuthorCounter { private int counter; private boolean isRelated; // standard constructors/getters public RelatedAuthorCounter accumulate(Author author) { if (author.getRelatedArticleId() == 0) { return isRelated ? this : new RelatedAuthorCounter( counter, true); } else { return isRelated ? new RelatedAuthorCounter(counter + 1, false) : this; } } public RelatedAuthorCounter combine(RelatedAuthorCounter RelatedAuthorCounter) { return new RelatedAuthorCounter( counter + RelatedAuthorCounter.counter, RelatedAuthorCounter.isRelated); } }

Setiap metode di kelas di atas melakukan operasi tertentu untuk menghitung saat melakukan traverse.

First, the accumulate() method traverse the authors one by one in an iterative way, then combine() sums two counters using their values. Finally, the getCounter() returns the counter.

Now, to test what we’ve done so far. Let’s convert our article's list of authors to a stream of authors:

Stream stream = article.getListOfAuthors().stream();

And implement a countAuthor() method to perform the reduction on the stream using RelatedAuthorCounter:

private int countAutors(Stream stream) { RelatedAuthorCounter wordCounter = stream.reduce( new RelatedAuthorCounter(0, true), RelatedAuthorCounter::accumulate, RelatedAuthorCounter::combine); return wordCounter.getCounter(); }

If we used a sequential stream the output will be as expected “count = 9”, however, the problem arises when we try to parallelize the operation.

Let's take a look at the following test case:

@Test void givenAStreamOfAuthors_whenProcessedInParallel_countProducesWrongOutput() { assertThat(Executor.countAutors(stream.parallel())).isGreaterThan(9); }

Apparently, something has gone wrong – splitting the stream at a random position caused an author to be counted twice.

4.2. How to Customize

To solve this, we need to implement a Spliterator that splits authors only when related id and articleId matches. Here’s the implementation of our custom Spliterator:

public class RelatedAuthorSpliterator implements Spliterator { private final List list; AtomicInteger current = new AtomicInteger(); // standard constructor/getters @Override public boolean tryAdvance(Consumer action) { action.accept(list.get(current.getAndIncrement())); return current.get() < list.size(); } @Override public Spliterator trySplit() { int currentSize = list.size() - current.get(); if (currentSize < 10) { return null; } for (int splitPos = currentSize / 2 + current.intValue(); splitPos < list.size(); splitPos++) { if (list.get(splitPos).getRelatedArticleId() == 0) { Spliterator spliterator = new RelatedAuthorSpliterator( list.subList(current.get(), splitPos)); current.set(splitPos); return spliterator; } } return null; } @Override public long estimateSize() { return list.size() - current.get(); } @Override public int characteristics() { return CONCURRENT; } }

Now applying countAuthors() method will give the correct output. The following code demonstrates that:

@Test public void givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() { Stream stream2 = StreamSupport.stream(spliterator, true); assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9); }

Also, the custom Spliterator is created from a list of authors and traverses through it by holding the current position.

Let’s discuss in more details the implementation of each method:

  • tryAdvance passes authors to the Consumer at the current index position and increments its position
  • trySplit defines the splitting mechanism, in our case, the RelatedAuthorSpliterator is created when ids matched, and the splitting divides the list into two parts
  • estimatedSize – is the difference between the list size and the position of currently iterated author
  • characteristics– returns the Spliterator characteristics, in our case SIZED as the value returned by the estimatedSize() method is exact; moreover, CONCURRENT indicates that the source of this Spliterator may be safely modified by other threads

5. Support for Primitive Values

The SpliteratorAPI supports primitive values including double, int and long.

The only difference between using a generic and a primitive dedicated Spliterator is the given Consumer and the type of the Spliterator.

Misalnya, ketika kita membutuhkannya untuk nilai int, kita perlu meneruskan intConsumer . Selanjutnya, inilah daftar Spliterator khusus primitif :

  • OfPrimitive : antarmuka induk untuk primitif lainnya
  • OfInt : Spliterator khusus untuk int
  • OfDouble : Spliterator yang didedikasikan untuk double
  • OfLong : Spliterator yang didedikasikan untuk waktu yang lama

6. Kesimpulan

Pada artikel ini, kami membahas penggunaan Java 8 Spliterator , metode, karakteristik, proses pemisahan, dukungan primitif, dan cara menyesuaikannya.

Seperti biasa, implementasi lengkap dari artikel ini dapat ditemukan di Github.