Spis treści

Java 17 - RandomGenerator i spółka

Dziś szybki rzut oka 👁️ na biblioteki przydatne podczas generowania losowych danych (przydają się podczas pisania testów oraz do prototypowania) i głębsze zanurzenie 🌊 w pakiet java.util.random.

Przyglądam się klasiejava.util.Random oraz pakietowi java.util.random w kontekście nowych interfejsów, jakie pojawiły się w Javie 17 (patrz Java 17 - co nowego).

Nowe interfejsy to m.in. RandomGenerator, RandomGenerator.StreamableGenerator, RandomGenerator.JumpableGenerator, RandomGenerator.SplittableGenerator.

Generowanie danych dla testów

Jeśli Twoje programy kiedykolwiek potrzebowały testów, to testy zapewne potrzebowały testowych danych. Masz szczęście, jeśli dane do testów to dane rzeczywiste (na przykład historyczne dane transakcji, operacji bankowych, zdarzeń w systemie).

Gorzej, jeśli musisz sobie radzić inaczej i chcesz wygenerować dane losowe.

W takiej sytuacji możesz się posiłkować gotowymi bibliotekami do generowania “mock data”:

Github Stars Page
Java Faker 2.8K ⭐ http://dius.github.io/java-faker
Easy Random 1.2K ⭐ https://github.com/j-easy/easy-random/wiki
Datagen 55 ⭐ http://qala.io/blog/randomized-testing.html

Czasem jednak możesz po prostu nabrać ochoty do eksperymentowania z własnymi pomysłami. Możesz więc zakasać rękawy i rzucić okiem na klasyczny java.util.Random lub przyjrzeć się nowym sposobom uzyskiwania generatorów w Javie 17: użyciu RandomGeneratorFactory czy wyborze generatora za pomocą nazwy algorytmu (np.: RandomGenerator g = RandomGenerator.of("L64X128MixRandom")).

Trochę historii

Przed 1.7: Random i SecureRandom

Przed Javą 1.7 istniały tylko dwie klasy: Random i klasa pochodna SecureRandom: /posts/java-17-random-generator/random_secure_random.png

Java 1.7: ThreadLocalRandom

W Javie 1.7 wprowadzona została nowa klasa pochodna od Random: ThreadLocalRandom, której instancje są lokalne dla bieżącego wątku. Choć klasa Random jest threadsafe, jej użycie groziło zmniejszeniem wydajności i wysokim contention wątków przy dostępie do instancji klast Random; klasa ThreadLocalRandom rozwiązuje ten problem.

/posts/java-17-random-generator/threadlocalrandom.png

Java 8

Java 8 i wprowadzenie do języka strumieni pozwoliło na rozszerzenie API klas Random i SecureRandom o metody strumieniujące losowe wartości z wariantami pozwalającymi na określenie ilości elementów w strumieniu bądź zakresu wartości (DoubleStream doubles(...), IntStream.ints(...), LongStream.longs(...)).

Java 8 wprowadziła również klasę SplittableRandom (niezależną w hierarchii dziedziczenia od klasy Random) z metodą split() zwracającą nową instancję klasy SplittableRandom posiadającą odrębny stan. Instancje te nie są threadsafe, a ich główne zastosowanie to użycie niezależnych obiektów generującyh w obliczeniach typu fork/join, np.

1
 var t = new Subtask(aSplittableRandom.split()).fork()

Niestety, klasy Random i SplittableRandom nie są “spokrewnione” ani nie posiadają wspólnego interfejsu, więc podmiana jednej klasy na drugą niesie ze sobą całkiem duże ryzyko refaktoryzacyjne.

/posts/java-17-random-generator/with_splittable_random.png

Java 17 - nowa hierarchia i nowe API

Interfejs Random Generator

Java 17 wprowadziła interfejs RandomGenerator, którego API definiuje wspólny protokół do generowania wartości.

Sekwencję wartości można uzyskać - podobnie jak w przypadku klas Random czy SecureRandom - na dwa sposoby: wielokrotnie wywołując poszczególne metody bądź wywołując jedną metodę zwracającą strumień (Stream) wartości. Wartości otrzymane drugim sposobem są wybierane w podobny sposób, lecz nie będą to te same liczby (r.ints(100) może zwrócić strumień innych wartości, niż strukrotne wywołanie r.nextInt()).

Mogą być generowane:

To główny i najważnieszy interfejs w pakiecie java.util.random. W podlinkowanej dokumentacji można poczytać o szczegółach API, typach generatorów i rodzajach wykorzytsywanych algorytmów. Są tam również sugestie dotyczące tego, jaki algorytm będzie odpowiedni do jakiego rodzaju aplikacji.

Ten interfejs został “wstecznie nałożony” na istniejące klasy, dzięki czemu programiści uzyskują klasyczną możliwość wymiany jednej implementacji generatora na inną, zależnie od bieżących potrzeb.

Interfejs RandomGenerator.StreamableGenerator

Ten interfejs definiuje metody do tworzenia strumieni obiektów typu RandomGenerator w taki sposób, aby możliwe było równoczesne ich użycie w wielu wątkach. Idea jest tutaj taka, żeby móc łatwo tworzyć statystycznie niezależne generatory (ta właściwość zachodzi z “bardzo dużym prawdopodobieństwem”) i przekazać je do różnych wątków, a nie współdzielić jeden generator, gdyż większość implementacji RandomGenerator po prostu nie jest thread safe.

Oto nowa hierarchia klas i interfejsów:

/posts/java-17-random-generator/classes_randomgenerator.png

Stany i cykle

Javadoc interfejsu RandomGenerator opisuje kontrakt, a może raczej założenie, jakie spełnia każdy obiekt ten interfejs implementujący:

  • każdy obiekt zawiera (albo raczej: jest zdefiniowany przez) pewien skończony stan
  • wygenerowanie pseudolosowej wartości jest równoznaczne ze zmianą stanu - przejściem do “nowego” stanu
  • wygenerowana wartość zależy wyłącznie od bieżącego stanu
  • liczba różnych stanów między którymi “wędruje” obiekt to okres
  • niektóre implementacje RandomGeneratora mogą być prawdziwie losowe, a nie pseudolosowe; wówczas stan zależy od statystycznego zachowania źródła “losowości” (fizycznego obiektu: termometru, procesora, innych urządzeń wejściowych) i ich okres nie musi być stały
/posts/java-17-random-generator/states.png

Klasa SplittableRandom implementuje RandomGenerator.StreamableGenerator

Klasa SplittableRandom implementuje interfejs RandomGenerator.StreamableGenerator z trzema metodami:

  • static RandomGenerator.StreamableGenerator of(String name) zwraca instancję StreamableGeneratora używającego podanego algorytmu
  • Stream<RandomGenerator> rngs() generuje nieskończony strumień obiektów implementujących RandomGenerator
  • default Stream<RandomGenerator> rngs(long streamSize) zwraca skończony strumień streamSize obiektów implementujących RandomGenerator. Główną klasą “wspomagającą” jest RandomGeneratorFactory.

RandomGeneratorFactory

Java 17 wprowadziła też nową klasę RandomGeneratorFactory, której API pozwala na tworzenie generatorów o właściwościach odpowiednich do rodzaju tworzonej aplikacji. API pozwala też na wybranie fabryki generatorów o specyficznych właściwościach.

Przykład: Oto kod, który tworzy fabrykę dostarczającą RandomGeneratory. Następnie tworzy i uruchamia dziesięć wątków. Każdy wątek uzyskuje z fabryki “swój” generator i używa go, losując liczbę do wypisania. Ponieważ za każdym razem do metody create podawane jest to samo ziarno (seed), powstające generatory będą generowały te same wartości. W poniższym przykładzie zostanie 10 razy wypisana ta sama wartość:

1
2
3
4
5
6
7
8
  RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Random");

     for (int i = 0; i < 10; i++) {
         new Thread(() -> {
             RandomGenerator random = factory.create(100L);
             System.out.println(random.nextDouble());
         }).start();
     }

Oto kod, któru pozwala na wyszukanie fabryki, która produkuje RandomGeneratory o najwyższej liczbie bitów stanu:

1
2
3
4
5
6
7
8
  RandomGeneratorFactory<RandomGenerator> best = RandomGeneratorFactory.all()
         .sorted(Comparator.comparingInt(RandomGenerator::stateBits).reversed())
         .findFirst()
         .orElse(RandomGeneratorFactory.of("Random"));
     System.out.println(best.name() + " in " + best.group() + " was selected");

     RandomGenerator rng = best.create();
     System.out.println(rng.nextLong());

Podsumowanie

Generowanie losowych wartości nie należy może do codziennych zadań programisty, ale kiedy już trzeba to zrobić, to warto mieć choćby pobieżną orientację w temacie:

  • RandomGenerator jest bazowym, wspólnym dla klas generujących interfejsem wprowadzonym w Javie 17
  • implementuje ją klasa Random, która jest threadsafe, lecz użycie jej przez wiele wątków może prowadzić do zmniejszającej wydajność rywalizacji (contention)
  • do rozwiązań kryptograficznych należy używać klasy SecureRandom
  • w aplikacjach wielowatkowych można używać bezpiecznych dla wątków (threadsafe) instancji ThreadLocalRandom (rozwiązuje problem contention)
  • jeśli wątki nie współdzielą generatora, to mogą również użyć instancji klasy SplittableRandom i metody split()
  • SplittableRandom może być również źródłem strumienia obiektów RandomGenerator dzięki metodom rngs
  • instancje generatorów o pewnych wymaganych właściwościach można również otrzymać korzystając z fabryki generatorów RandomGeneratorFactory

Mam nadzieję, że artykuł zachęcił Cię do zajrzenia do dokumentacji i poczytania o istniejących oraz tych dodanych w Javie 17 mechanizmach generowania wartości.

Miłedo kodowania!

PS

Zdjęcie kostek do gry: @mickhaupt