Wybór języka

Odpowiedź do małej łamigłówki z Javy

Ene, due, like…

Konkurs już się zakończył, nagrody zostały przyznane… I choć szkolenia toczą się nadal swoim trybem, to można już chyba bez obaw opublikować rozwiązanie prostej zagadki. I okrasić stosownym komentarzem.

Minimalna wersja Javy™ potrzebna do uruchomienia tego kodu to 10. Powody są dwa: obecność var oraz wywołanie List.copyOf. Wszystkie przeładowane warianty List.of wjechały w Javie 9.

Żeby odpowiedzieć na drugie pytanie, to wciąż nie trzeba uruchamiać kodu zagadki, wystarczy wnikliwie poczytać dokumentację List.of() i List.copyOf(). (Wciąż masz szansę, odnośniki są poprzednim akapicie. ;-)) Nasze raz będzie listą niemodyfikowalną, bo javadoc mówi, że of() returns an unmodifiable list. Czyli taką listę, do której już nie można dodać i z której nie można już usunąć elementów (bo same elementy można zmieniać “w środku”, jeśli są modyfikowalne). Podobnie rzecz ma się z copyOf(): rezultat też jest niemodyfikowalny. Natomiast javadoc od copyOf() stwierdza w implementation note (tak, aż tam trzeba dojść), że if the given Collection is an unmodifiable List, calling copyOf will generally not create a copy: jeśli kolekcja podana do skopiowania jest już listą niemodyfikowalną, to nie jest tworzona kopia, tylko zwracany jest oryginał. Dlatego dwa nie tylko jest taką samą listą co raz, ale jest dokładnie tą samą listą, tym samym obiektem. I w związku z tym raz == dwa to szczera prawda w tej zagadce, co skutkuje wyświetleniem ene! Gdyby nie else, to due też byśmy zobaczyli, bo w końcu x.equals(x) skutkuje true.

Riddle run in jshell
Trzecie pytanie było oznaczone gwiazdką z kilku powodów:

  • żeby na nie odpowiedzieć, to już trzeba zagadkę uruchomić lub zajrzeć do kodu Javy,
  • to jest w zasadzie szczegół implementacyjny, który nie powinien nas za bardzo interesować,
  • dziś są możliwe przynajmniej dwie poprawne odpowiedzi, w przyszłości może ich być więcej (patrz punkt wyżej).

Jeśli uruchomić ten kod na Javie 11 lub nowszej (na razie do 15 włącznie) to raz jest instancją klasy java.util.ImmutableCollections.List12. List12 jest klasą zagnieżdżoną klasy ImmutableCollections, a i sama klasa ImmutableCollections dostępna jest wyłącznie w pakiecie java.util, więc nie da się w kodzie stworzyć List12 przez konstruktor. Analizę List12 zacząłbym od nazwy. Na moje oko (i ucho) to ta klasa nazywa się (choć nikt mi tego nie mówił) “list one-two”, nie “list twelve”. A dlaczego? Ano dlatego (i już warto popatrzeć w jej kod), że jest to klasa, której instancje są w stanie przechowywać dokładnie jeden lub dwa obiekty. Co więcej, nie wykorzystują do tego tablicy czy jakichś dynamicznych dowiązań, tylko sztywno, już od momentu utworzenia, trzymają albo jeden, albo dwa elementy, tertium non datur. Słowem, Java też już ma wyspecjalizowane klasy kolekcji do przechowywania małej liczby elementów (patrz Set12). Jak sugerowałem w CONTEXTVS, STVLTE! podając przykład Scali, małe kolekcje spotyka się w przyrodzie na tyle często, że warto mieć ich osobne implementacje, choćby z powodów wydajnościowych. Przykład? Sprawdzenie liczby elementów w List12 jest banalnie proste: jeśli referencja do drugiego elementu jest nullem, to wtedy mamy jeden element, w pozostałych przypadkach mamy dwa. Banalnie proste i wściekle wydajne, nie trzeba sprawdzać długości tablicy czy cache’ować rozmiaru dla wywołań size().

Jeszcze dla porządku trzeba powiedzieć o drugiej możliwości dla trzeciego pytania. Otóż w Javie 10 nie istniała klasa List12, tylko dwie osobne klasy List1 oraz List2. Ale to szczegół implementacyjny, który może się zmieniać, bo nie jest elementem publicznego API.

Panie, ale co z tego?

Cała ta historia ma moim zdaniem drugie dno, którego nigdzie nie widziałem oficjalnie, ale którego się domyślam, patrząc na zmiany w kolejnych wersjach Javy od czasu 8. Odczucie moje jest takie, że Java po prostu jest popychana coraz bardziej w stronę języków, które umożliwiają programowanie w stylu deklaratywnym i funkcyjnym (nie tylko imperatywnym). Jednym z kluczowych aspektów stylu funkcyjnego jest brak efektów ubocznych. Trzymanie danych w strukturach niezmienialnych elegancko tu pomaga. O zyskach w łatwości czytania takiego kodu, łatwości programowania defensywnego itd. długo by mówić. Argument “ale to wydajność siądzie” można zbyć “a widzisz, jak szybko copyOf() działa?” lub “mamy wściekle szybkie klasy na małe, bardzo popularne kolekcje”, poza tym sprzęt mamy coraz szybszy i szybszy. “Nowe” klasy z java.time? Niezmienialne. Rekordy? Niezmienialne. Streamy? Do jednokrotnego użytku.

Musimy pójść głębiej

A jak pójść nawet głębiej, to wychodzi kolejne dno. Omawianą zagadkę zadaję m.in. na początku mojego szkolenia z efektywnego korzystania z nowinek, które są dostępne od Javy 8-9. Gdy bowiem przed tą zagadką zapytać uczestników “jaka jest u was wersja Javy?”, to często słychać gromkie “JEDENAŚCIE!!” Poźniej, już po zagadce i w trakcie szkolenia albo przeglądu kodu, wychodzi na to, że 11 owszem, występuje w krajobrazie, ale głównie jest to numerek JVM na produkcji, ale nie wersja, w której myślimy i pod którą programujemy. Okazuje się, że lata edukacji, certyfikatów, rachitycznych tutoriali i antycznych już odpowiedzi na StackOverflow wyżarły nam, programistom, takie dziury w umysłach, że teraz trudno wypędzić z nich pętlozę, ifologię, nieaktualne wzorce projektowe, mutowalność wszystkiego i zawsze. Więcej, świeży absolwenci naszych światłych uczelni i “życiowych” bootcampów są replikowani na obraz i podobieństwo ich wykładowców i mentorów. Jest sympatycznie, gdy spotka się coś poza collect(toList()), a collect(toUnmodifiableList()) to już prawdziwy delikates. Stąd wysnuwam wniosek: radośnie ustawiamy kompilator javac na --target 11, ale nasze nawyki i zwoje mózgowe niekoniecznie zostały przestawione na te same tory. Wciąż spotyka się dużo ogłoszeń pt. “programista JAVA 8 potrzebny na już”. I żeby nie było: od Javy 8 odzyskałem wiarę w tę technologię, że nie będzie wolno dryfującą krypą na mielizny COBOLowe, z których trzeba będzie skakać na wodoloty Kotlin czy Scala. Java 8 jest trochę jak parowóz przed laty. Nadal to silna maszyna, rzetelny mechaniczny koń pociągowy. I nie wszędzie maglevy mają rację bytu. Tylko czy do końca życia chcesz jeździć parowozem? Może jednak lokomotywa elektryczna ma sens? Proszę nie zrozumieć mnie opacznie, to nie jest tak, że stary kod jest przeklęty z definicji i musi zostać przepisany na nowiuśkie wersje. Chodzi o to, że co miało sens dla Javy 7, dziś niekoniecznie musi mieć sens. Może pora zaadoptować w nas uwspółcześnione postrzeganie Javy.

… i morele bęc!!!

Wybór języka