Wybór języka

JEP 445 i krótsze Java skrypty

Poprzednio umieściłem na moim blogu dwa wpisy. Jeden dotyczył Java skryptów, drugi możliwości ich przenoszenia poprzez wykorzystanie shebang.

O ile mogę stwierdzić, wszystko w tych dwóch częściach nadal jest aktualne, jednak teraz, w Javie 21, dzięki JEP-445, poprzednie dwa wpisy mogłyby być jeszcze bardziej zwięzłe. W tym wpisie spróbuję opisać najpierw zmiany wprowadzone przez ten JEP, a następnie zajmę się bardziej szczegółowo tym, w jaki sposób mają się do “skryptów w Javie”.

Opiszę, jak można uprościć metodę main, pozbyć się otaczającej jej klasy, a potem zobaczymy, kiedy to nie będzie działać.

Czy metoda main jest naprawdę zbyt rozlazła?

Zacząłem programować w Javie w 2003 roku. Java nie była moim pierwszym językiem pochodzącym od C, którym się posługiwałem. Przed Javą znałem Turbo Pascal i C++, więc koncepcja int main() jako punktu wejścia nie była dla mnie zupełnie nowa. Z drugiej strony byłem też dość wydajnym programistą PHP 3 (ponieważ stypendium nie wystarczało), więc również miałem styczność z koncepcją języka programowania bez main. (Tak, uważam, że PHP to język programowania i zasługuje na szacunek, gdy trzeba).

W rzeczywistości w tamtych czasach Java była pierwszym językiem praktycznie dla nikogo, już znaliśmy kilka języków z main. Oczywiście, String[] args było pewnym ceremoniałem, ale to było w porządku. To, co było nieco bardziej zagmatwane i wymagało podejścia “zaufaj mi, zrozumiesz to później”, to klasa otaczająca main. Pisaliśmy zatem class Klass, aby zrozumieć to później, chyba że mieliśmy już doświadczenie z obiektowym C++.

Rzecz w tym, że to było 20 lat temu, kiedy Java była postrzegana jako nowinka. W dzisiejszych czasach mamy coraz więcej nowicjuszy w Javie, którzy pochodzą z języków bez main lub nawet nie mają w ogóle doświadczenia w programowaniu. Dlatego bardzo cenię to, co umożliwia im JEP-445, czyli pominięcie części ceremoniału, gdy tworzą prosty program w jednym pliku, ponieważ uczą się wszystkich tych “zmiennych” i “pętli”, a być może nawet “metod”.

Jeśli jnie przeszkadza ci --enable-preview i nie potrzebujesz argumentów, zamiast:

public class PublicStaticVoidMainWithArguments {
    public static void main(String[] _ignored) {
        System.out.println("Good old public static main.");
    }
}

możesz po prostu napisać:

void main() {
    System.out.println("The new main.");
}

Teraz, dzięki JEP-445, możemy:

  • W ogóle pominąć deklarację klasy,
  • Pominąć też String[] args (chyba że nadal ich potrzebujesz, wtedy nadal możesz ich używać, oczywiście),
  • Pozbyć się static z metody main, więc może to być metoda instancyjna,
  • W rzeczywistości możesz użyć dowolnego modyfikatora dostępu, włącznie z domyślnym (tak jak wyżej), z wyjątkiem private.

Priorytet metod main

“No dobra” możesz zapytać, “w Javie mogę przeładowywać metody. Co się stanie, jeśli mam public static void main(String args[]) i void main()?”

Cóż, nie martw się. Jeśli kod się kompiluje i masz dwie metody main, tylko jedna zostanie wywołana, a priorytet jest ściśle określony.

  • Najpierw metoda z String[] args ma pierwszeństwo przed metodą bez args.
  • Następnie static jest preferowane nad instancyjnym.
  • Na koniec, im bardziej “public”, tym wyżej jest na liście.

Przy okazji, zastanawiam się, czy to naprawdę sensowne podejście, aby mieć taką łamigłówkę z więcej niż jedną metodą main w swoim pliku (lub klasie). Nie rób tego sobie i ludziom wokół siebie. ;-)

Czy to podejście bez klasy?

Niekoniecznie. Klasa nadal istnieje. Jest to tzw. klasa nienazwana (ang. unnamed class), generowana dla nas automatycznie pod spodem.

Nie musisz mi wierzyć, możesz zaufać kodowi. Spróbuj umieścić poniższy kod w pliku o rozszerzeniu .java i uruchom go:

void main() {
    System.out.println("New instance main in unnamed class, without arg(s).");
    System.out.println("Is the class unnamed? " + this.getClass().isUnnamedClass());
    System.out.println("The class name is " + this.getClass().getName() + "...?");
    System.out.println("And the modifiers are [" + java.lang.reflect.Modifier.toString(this.getClass().getModifiers())+"]");
    System.out.println("(The package name is [" +this.getClass().getPackage().getName()+"])");
}

Jeśli się zastanawiasz, jak uruchomić ten kod (np. dlatego, że Twoje IDE nie pokazuje Ci tego ładnego zielonego przycisku “Uruchom”), zawsze możesz uruchomić to tak, jak umożliwia JEP-330:

$ java --source 21 --enable-preview nazwaTwojegoPliku.java

Zobaczysz, że klasa jest final, znajduje się w domyślnym (“nienazwanym”) pakiecie, teraz możesz sprawdzić, czy jest “unnamed class” (nienazwana) oraz że… ma nazwę! Jej nazwa pochodzi od nazwy pliku. Zabawne jest to, że klasa faktycznie ma nazwę, pochodzącą od nazwy pliku… To jest taki “szczegół implementacyjny”…

Dlatego, jeśli nadałeś swojemu plikowi na przykład nazwę nazwa-twojego-pliku.java, zobaczysz błąd. Znowu, nie musisz mi wierzyć, uruchom i baw się dobrze ;-)

Jeśli z jakiegoś powodu nie możesz tego zrobić, wygląda to mniej więcej tak:

your-file-name.java:44: error: bad file name: your-file-name
    void main() {
         ^

Aktualnie nazwa pliku nie może być po prostu dowolną nazwą, nadal musi być poprawną nazwą klasy, jeśli nazwa pliku kończy się na .java. (Albo musisz go umieścić w klasie o modyfikatorze dostępu niepublicznym). Nie jestem pewien, czy to szczęśliwy zbieg okoliczności, bo ma to pewne konsekwencje.

Konsekwencja: brak skryptów bez klasy

Po JEP-330 byliśmy w stanie pisać “skrypty w Javie” za pomocą #!shebang.

Może nas teraz korcić, żeby umieścić powyższą metodę main w pliku np. o nazwie java.script i dodać na początku linię:

#!/usr/bin/env -S java --enable-preview --source 21

Tylko… to nie działa. Mechanizm przekształcania nazwy pliku na nazwę klasy ma zastosowanie tylko wtedy, gdy uruchamiamy java --source effectivelyAKlass.java. Jeśli używamy shebang, to nie zadziała. Znowu, z powodu błędu “bad file name”. Musimy umieścić “uproszczoną” metodę main w klasie, więc wygląda to na przykład tak:

#!/usr/bin/env -S java --enable-preview --source 21

// In GNU/Linux the -S can be used since env version 8.30
// for other OSes and older env versions you may need to use absolute shebang

class KlasaPosiadająca {
     void main() {
        System.out.println("This is Java Script! 👿");
        System.out.println("Hello from "+ Runtime.version());
        System.out.println("New instance main in unnamed class, without arg(s).");
        System.out.println("Is the class unnamed? " + this.getClass().isUnnamedClass());
        System.out.println("The class name is " + this.getClass().getName() + "...?");
        System.out.println("And the modifiers are [" + java.lang.reflect.Modifier.toString(this.getClass().getModifiers())+"]");
        System.out.println("(The package name is [" +this.getClass().getPackage().getName()+"])");
    }
}

I nie bardzo możemy się pozbyć tej klasy posiadającej, jeśli ten skrypt ma działać…

Czy podoba mi się ten aspekt JEP-445?

Szczerze mówiąc, nie jestem pewien, czy podoba mi się ten “szczegół implementacyjny”, który w rzeczywistości zmusza nas do nadawania odpowiednich nazw plikom .java lub deklarowania w skryptach shebang class Klass.

Oczywiście zdaję sobie sprawę, że to jest wyjątkowo rzadki przypadek, brzeg brzegów, przydatny głównie dla koneserów rozmów rekrutacyjnych (kiedy odwracanie drzew binarnych już nie wystarcza). I może się okazać, że umykają mi poważne argumenty, które czynią to podejście najlepszym…

Mimo to może ten “szczegół implementacyjny” jest zbyt restrykcyjny? Może automatycznie generowana klasa mogłaby mieć pustą nazwę "" tak jak pakiet domyślny, jeśli to nie wymagałoby zbyt dużych zmian w JVMie? A jeśli to byłby zbyt duży krok, to może przynajmniej mogłaby mieć całkowicie losową nazwę? Albo może chęć poznania nazwy klasy nienazwanej powinna rzucać wyjątek, tak jak addFirst(...) w kolekcjach niezmiennych? (Chociaż naprawdę nie jestem pewien, czy to byłby dobry pomysł).

Obawiam się nieco, że ten szczegół implementacyjny nie tylko może być zbyt restrykcyjny, ale niektórzy ludzie mogą go nadużywać, nawet jeśli dokumentacja mówi, że nie należy używać nazwy UnnamedClass. (Obiło ci się o uszy sun.misc.Unsafe? ;-))

Przyszłość pokaże.

I tak jest dużo lepiej

Stary skrypt

Co prawda, jeśli chcemy napisać “porządny skrypt w Javie”, nadal musimy korzystać z klasy albo mieć właściwą nazwę pliku, ale i tak jest lepiej. Przynajmniej dużo łatwiej teraz zacząć przygodę z Javą.

Nie mam jakichś szczególnie dużych złudzeń, że takie skrypty będą popularne na produkcji w czasach obrazów natywnych. Ale do nauki powinny być w sam raz, bo nie trzeba od razu rozumieć wszystkiego (albo brać na wiarę) i do tego męczyć się z javac i java.
A to jeszcze nie jest ostatnie słowo w skryptach Javy, dlatego że jest propozycja kolejnego JEPa-458!

Wybór języka