Pengenalan kepada Spliterator di Jawa

1. Gambaran keseluruhan

Antara muka Spliterator , yang diperkenalkan di Java 8, dapat digunakan untuk melintasi dan memisahkan urutan . Ini adalah utiliti asas untuk Aliran , terutamanya yang selari.

Dalam artikel ini, kita akan membahas penggunaan, ciri, kaedah dan cara membuat pelaksanaan tersuai kita sendiri.

2. API Spliterator

2.1. cubaAdvance

Ini adalah kaedah utama yang digunakan untuk melangkah mengikut urutan. Kaedah ini menggunakan Pengguna yang digunakan untuk menggunakan unsur-unsur Spliterator satu demi satu secara berurutan dan kembali palsu jika tidak ada unsur yang akan dilalui.

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

Pertama, anggap bahawa kita mempunyai ArrayList dengan 35000 artikel dan kelas Artikel ditakrifkan sebagai:

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

Sekarang, mari kita laksanakan tugas yang memproses senarai artikel dan menambahkan akhiran " - diterbitkan oleh Baeldung" untuk 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 bahawa tugas ini menghasilkan jumlah artikel yang diproses ketika selesai pelaksanaan.

Titik utama lain ialah kami menggunakan kaedah tryAdvance () untuk memproses elemen seterusnya.

2.2. cubaSplit

Seterusnya, mari kita bahagikan Spliterators (maka namanya) dan proses partisi secara bebas.

The trySplit kaedah cuba untuk berpecah kepada dua bahagian. Kemudian elemen proses pemanggil, dan akhirnya, contoh yang dikembalikan akan memproses yang lain, yang memungkinkan kedua diproses secara selari.

Mari buat senarai kami terlebih dahulu:

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

Seterusnya, kami memperoleh contoh Spliterator kami menggunakan kaedah spliterator () . Kemudian kami menggunakan kaedah 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 berjalan seperti yang diinginkan dan membahagikan catatan sama .

2.3. diukurSaiz

The estimatedSize kaedah memberikan kita anggaran bilangan unsur:

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

Ini akan menghasilkan:

Size: 17500

2.4. mempunyaiCiri-ciri

API ini memeriksa sama ada ciri yang diberikan sesuai dengan sifat Spliterator. Kemudian jika kita menggunakan kaedah di atas, output akan menjadi representasi int dari ciri-ciri tersebut:

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

3. Ciri-ciri Spliterator

Ia mempunyai lapan ciri berbeza yang menggambarkan tingkah lakunya. Itu boleh dijadikan petunjuk untuk alat luaran:

  • BERSAIZ - jika ia mampu kembali nombor yang tepat unsur-unsur dengan estimateSize () kaedah
  • DIJUAL - jika ia berulang melalui sumber yang disusun
  • SUBSIZED - jika kita membagi instance menggunakan kaedah trySplit () dan mendapatkan Spliterators yang BERSAIZ juga
  • SEMASA - jika sumber dapat diubah suai secara serentak
  • DISTINCT - jika untuk setiap pasangan unsur yang ditemui x, y,! X. sama (y)
  • TIDAK BOLEH - jika elemen yang dipegang oleh sumber tidak dapat diubah secara struktur
  • BUKAN - jika sumber menahan nol atau tidak
  • DIPESAN - jika berulang mengikut urutan yang disusun

4. Pemisah Custom

4.1. Bilakah Menyesuaikan

Pertama, mari kita anggap senario berikut:

Kami mempunyai kelas artikel dengan senarai pengarang, dan artikel yang boleh mempunyai lebih daripada satu pengarang. Selanjutnya, kami mempertimbangkan pengarang yang berkaitan dengan artikel sekiranya id artikelnya yang berkaitan sesuai dengan id artikel.

Kelas Pengarang kami akan kelihatan seperti ini:

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

Seterusnya, kami akan melaksanakan kelas untuk mengira pengarang sambil melintasi aliran pengarang. Kemudian kelas akan melakukan pengurangan di aliran.

Mari kita lihat pelaksanaan 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 kaedah dalam kelas di atas melakukan operasi khusus untuk dikira semasa melintasi.

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.

Sebagai contoh, apabila kita memerlukannya untuk nilai int, kita perlu melepasi intConsumer . Selanjutnya, berikut adalah senarai Spliterator khusus primitif :

  • Daripada Primitif : antara muka ibu bapa untuk primitif lain
  • OfInt : Pemisah khas untuk int
  • OfDouble : Pemisah yang didedikasikan untuk dua kali ganda
  • OfLong : Pemisah yang didedikasikan untuk masa yang lama

6. Kesimpulannya

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

Seperti biasa, pelaksanaan sepenuhnya artikel ini dapat dilihat di Github.