var, czyli mniej grafomaństwa w Javie

Piotr Przybył 22 sierpnia 2019
 

W tym wpisie dowiesz się jak od Javy 10 wstukać mniej znaków, żeby napisać taki sam program.

Jak to dawniej bywało

Już starożytni zauważyli, że… No dobrze, starożytni mieli wiele celnych spostrzeżeń, ale raczej trudno ich wnioski bezpośrednio odnosić do współczesnych języków programowania. Jeśli mamy się odwoływać do starożytności, to przede wszystkim rzućmy okiem do starożytnej Sparty, gdzie (jak wieść gminna niesie) ćwiczono młodych adeptów sztuki wojennej w takim wysławianiu się i przekazywaniu informacji, żeby było krótko, konkretnie i na temat. Bez zbędnych ozdobników. Stąd dziś mamy “mówić lakonicznie” (Sparta była stolicą Lakonii) lub “w krótkich żołnierskich słowach”. Na wojnie bowiem nie ma czasu na krasomówstwo.

No dobrze, ale przecież można też walczyć z czasem, nawałem wymagań, itd. Każdy programista raczej prędzej niż później zetknie się z odwiecznym “dlaczego tak długo”. I imamy się wtedy różnych sposobów, żeby tylko nie było aż “tak długo”.

Niestety, Java kilka wersji temu należała do tych języków, w których trzeba dużo pisać, żeby coś napisać. Tzw. stosunek szumu do sygnału (ang. noise to signal ratio) nie wypadał zbyt korzystnie. Z tego powodu bywało, że programiści innych technologii nie mieli o Javie najlepszego zdania. Jeden z nich, wcześniej programujących w Adzie, stwierdził: “Java to język dla grafomanów!” I było w tym sporo racji.

Na szczęście zostało to dostrzeżone przez twórców Javy i w ostatnich wersjach jest coraz lepiej i lepiej. W tym wpisie pominiemy wprowadzenie lambd i referencji do metod (np. zamiast klas anonimowych), skupimy się za to na obecnym od Javy 10 var. (Proszę nie mylić z plikami WAR.)

Trochę historii dla lepszego zrozumienia tematu powinno pomóc. Zwłaszcza w sytuacji gdy przyjdzie Ci pracować w projekcie, który ciągnie się od wielu wersji i w którym z nie przepisano całego kodu.

W tej podróży przez czas i wersje weźmiemy na tapet popularną konstukcję, jaką jest tworzenie listy napisów w zmiennej lokalnej.

Java 1.4 i wcześniej

Java 1.4 i wcześniejsze nie obsługiwały tzw. typów generycznych (ang. generic types), w związku z tym kolekcje nie przechowywały informacji o typie przechowywanych elementów. Poniżej przykład metody, która zwraca listę z nazwami dni tygodnia.

List podajDniTygodnia_1_4() {
    List dniTygodnia = new ArrayList();
    dniTygodnia.add("poniedziałek");
    dniTygodnia.add("wtorek");
    dniTygodnia.add("środa");
    dniTygodnia.add("czwartek");
    dniTygodnia.add("piątek");
    dniTygodnia.add("sobota");
    dniTygodnia.add("niedziela");
    return dniTygodnia;
}

(Oczywiście nie jest to najlepszy sposób wypełniania listy danymi, jest tutaj użyty wyłącznie w celu demonstracji.)

Kłopot z takim podejściem polega na tym, że jeśli skupić się tylko na typie zwracanym przez metodę, to nie wiadomo, czego to jest lista. Co prawda nazwa metody pozwala snuć pewne domysły, że mogą to być napisy, ale kompilator nie jest w stanie sprawdzić takich domysłów. (Sprawy wyglądały jeszcze gorzej, gdy metoda nazywała się “getDays”. Dlatego nigdy nie nadawaj bezsensownych i zbyt lakonicznych nazw metodom.) Dlatego kompilator pozwalał na rzeczy w stylu:

// Andrzej, to walnie przy wywołaniu!
podajDniTygodnia_1_4().add(Integer.valueOf(42));

“To szaleństwo” z instanceof w roli głównej postanowiono zakończyć w Javie 1.5.

Java 1.5 i typy generyczne

Od wersji 1.5 w Javie zagościły tzw. typy generyczne (ang. generics lub generic types) i metodę należało napisać tak:

  List<String> podajDniTygodnia_1_5() {
      List<String> dniTygodnia = new ArrayList<String>();
      dniTygodnia.add("poniedziałek");
      // tu dodajemy resztę dni
      return dniTygodnia;
  }

I wtedy kompilator był już w stanie sprawdzić typy i zapobiec głupim wywołaniom w stylu

// Andrzej, to... się nie skompiluje
podajDniTygodnia_1_5().add(Integer.valueOf(42));

Przynajmniej w sytuacji gdy metoda była w naszym kodzie, bo wymazywanie typów (ang. type erasure) (ze względu na kompatybilność kodu bajtowego z poprzednich wersji) nadal działa “po kompilacji”.

Niestety, coś za coś. Żeby kompilator mógł sprawdzić poprawność typów, trzeba było te typy podać, dlatego w trzech miejscach należało (lub przynajmniej wypadało, żeby kompilator nie generował ostrzeżeń) napisać <String>. Grafomania, co tu dużo mówić…

Dociekliwi zaczęli drążyć temat, dlaczego w poniższej linii

List<String> dniTygodnia = new ArrayList<String>();

trzeba podawać typ parametryzujący aż dwa razy. “Czy kompilator nie może się domyślić, że jak chcę List i zaraz później tworzę jakąś listę, to jest to lista napisów???" Czy kompilator nie może sam wyciągać wniosków na temat typów?

Java 7 i wnioskowanie typów

Twórcy Javy w wersji 7 rozbudowali kompilator tak, by mógł sam przeprowadzić tzw. wnioskowanie typów (ang. type inference) w przypadku takiego zapisu. Po zastosowaniu ulepszeń metoda z przykładu wygląda następująco:

   List<String> podajDniTygodnia_7() {
       List<String> dniTygodnia = new ArrayList<>();
       dniTygodnia.add("poniedziałek");
       // tu dodajemy resztę dni
       return dniTygodnia;
   }

Zauważ, że w tym wypadku kompilator nadal “wie”, że chodzi o listę napisów, i nie wymaga podawania typów expresis verbis kolejny raz. Wystarczy podać o jaką listę chodzi (w tym przypadku `ArrayList<>) i już.

Dociekliwym to nie wystarczało ;-) Szczególnie piszącym w językach, które nie wymagają podania typu zmiennej dniTygodnia i pozwalają je zastąpić zwięzłym var.

Java 10, var wkracza do akcji

Dociekliwi rzecz stawiali następująco: “przecież w zakresie tej metody to wiadomo, że to jest jakaś lista, czy trzeba pisać dwa razy o liście?” Rzeczywiście, od wersji 10 nie trzeba. Można napisać tak:

   List<String> podajDniTygodnia_10() {
       var dniTygodnia = new ArrayList<String>();
       dniTygodnia.add("poniedziałek");
       // tu dodajemy resztę dni
       return dniTygodnia;
   }

Mamy tu przykład zastosowania “zarezerwowanego określenia/nazwy typu” (ang. reserver type name) var. Wykorzystanie var dla zmiennych lokalnych pozwala jeszcze bardziej ograniczyć grafomaństwo. Zwróć uwagę, że <String> teraz występuje po prawej stronie.

var nie jest słowem kluczowym

W tym miejscu należy zauważyć, że var nie jest słowem kluczowym. (To może mieć znaczenie, jeśli nie chcesz spalić rozmowy rekrutacyjnej.)

Ma Pan dowód? Oczywiście! Możesz odpalić w jshellu (o którym mowa w poprzednim wpisie) następującą linię:

var var = "Make love, not war"

Rezultat wygląda tak:

jshell> var var = "Make love, not war"
var ==> "Make love, not war"

Gdyby oznaczenie typu var było słowem kluczowym (jak np. break), wówczas jshell napisałby tak:

jshell> var break = "Gimme a break!"
|  Error:
|  ';' expected
|  var break = "Gimme a break!"
|     ^
|  Error:
|  ';' expected
|  var break = "Gimme a break!"
|           ^
|  Error:
|  break outside switch or loop
|  var break = "Gimme a break!"
|      ^----^

Oczywiście należy podkreślić, że nadawanie zmiennym nazw w stylu var nie jest dobrym pomysłem. Ale skoro można to zrobić, to nie jest to słowo kluczowe.

Gdzie var, a gdzie nie?

W telegraficznym skrócie: oznaczenie var można wykorzystać zamiast podawania typu dla zmiennej lokalnej w sytuacji, gdy zmienna ta jest od razu inicjalizowana.

Próby niepoprawnego wykorzystania var ilustruje poniższy kod:

class NaukaVar {

    var test = 12;

    // Andrzeju, nie denerwuj się, jeszcze się uczę o var
    private var metodaDoNaukiVar(var imię, var nazwisko) {
        var imięINazwisko = null;
        imięINazwisko = imię + " " + nazwisko;
        return imięINazwisko;
    }
}

Tego kodu nie da się skompilować z kilku powodów.

  • Typ pola klasy musi być podany jawnie, nie można wykorzystać var, jak np. w zapisie var test = 12;
  • Typ zwracany przez metodę musi być podany jawnie, czyli nie można napisać private var metodaDoNaukiVar.
  • Typy parametrów metod muszą być podane jawnie, zapis listy parametrów w postaci (var imię, var nazwisko) jest niepoprawny.
  • var zadziała dla zmiennych lokalnych (jak np. var imięINazwisko) tylko wtedy, gdy od razu zostanie zainicjalizowany konkretną zmienną.

Poprawny przykład na var w akcji wygląda tak:

class WykorzystanieVar {

    int test = 12;

    String połączImięINazwisko(String imię, String nazwisko) {
        var imięINazwisko = "";
        imięINazwisko = imię + " " + nazwisko;
        return imięINazwisko;
    }
}

Hulaj dusza, typu nie ma?

Czy wprowadzenie var dla zmiennych lokalnych oznacza, że teraz nie mają one typu? Albo że ten typ jest dynamiczny? Nic bardziej mylnego. Typy w Javie nie zmieniły się nic a nic, nadal są sprawdzane w momencie kompilacji i zapisywane w kodzie bajtowym (ang. byte code). Zmienna oznaczona var nadal ma precyzyjnie określony typ, tyle że automatycznie, nie ręcznie. Co więcej, jest to dokładnie ten sam typ, który mamy po prawej stronie przypisania.

Popatrzmy raz jeszcze na przykład z dniami tygodnia:

   List<String> podajDniTygodnia_10() {
       var dniTygodnia = new ArrayList<String>();
        // tu wypełniamy listę
       return dniTygodnia;
   }

Jaki typ posiada zmienna dniTygodnia? Będzie to nadal List? Może Collection (skoro lista jest kolekcją), albo nawet Object? Sprawdźmy to w jshellu:

jshell> var dniTygodnia = new ArrayList<String>()
dniTygodnia ==> []

jshell> dniTygodnia.getClass().getCanonicalName()
$5 ==> "java.util.ArrayList"

Jak widać, kompilator przypisuje zmiennej oznaczonej var ten sam konkretny typ, który jest wykorzystywany do inicjalizacji. I właśnie z tego powodu (oraz z chęci uniknięcia zgadywania “a co tam jest w środku” jak w Javie 1.4) wnioskowanie typu działa tylko dla zmiennych lokalnych, które są inicjalizowane w momencie deklaracji.

Nie stoi to jednak na przeszkodzie, by wykorzystywać var także w innych miejscach (pod warunkiem, że zmienne nadal są lokalne), jak np. pętle czy konstrucje “try-with-resource”, co ilustruje poniższy kod:

String połączNaiwnieNapisy(List<String> napisy) {
    var wynik = "";
    for (var iterator = napisy.iterator(); iterator.hasNext(); ) {
        wynik += iterator.next();
    }
    return wynik;
}

String odczytajPierwsząLinięZPliku(String ścieżkaDoPliku) throws IOException {
    try(var reader = new BufferedReader(new FileReader(ścieżkaDoPliku))) {
        return reader.readLine();
    }
}

A gdzie jest val?

Dociekliwych interesuje także, skoro od Javy 10 jest var, to czy pojawiło się także val? Nie, jak na razie nie ma val, const , let i innych podobnych konstrukcji. Natomiast nic nie stoi na przeszkodzie, żeby wykorzystać stare dobre final i pisać final var, np.

void gdzieJestVal() {
        // nie ma val, ale jest final var
        final var stała = "ostatnie słowo";
        // i działa, bo poniższa linia się nie kompiluje
        stała = "może jednak nie?";
    }

W następnym odcinku

Dociekliwi czytelnicy zauważyli już pewnie, że zamiast pisać

List<String> podajDniTygodnia_10() {
    var dniTygodnia = new ArrayList<String>();
    dniTygodnia.add("poniedziałek");
    dniTygodnia.add("wtorek");
    dniTygodnia.add("środa");
    dniTygodnia.add("czwartek");
    dniTygodnia.add("piątek");
    dniTygodnia.add("sobota");
    dniTygodnia.add("niedziela");
    return dniTygodnia;
}

można napisać

List podajDniTygodnia_10() { var dniTygodnia = Arrays.asList( "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"); return dniTygodnia; }

Tylko po co się męczyć, skoro zamiast tego można od Javy 9 napisać:

List.of("poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela");

Ale o tym już w kolejnym wpisie…

Piotr Przybył

Notoryczny inżynier w pracy i poza nią, podążający za meandrami sztuki programowania. Zawodowo Remote Freelance Software Gardener, od kilku lat wyrywający chwasty w ogródkach webowych i zwykle przycinający Javę do kształtów pożądanych przez klientów. Miłośnik lekkości i zwinności, która powinna przejawiać się przede wszystkim w stosowaniu właściwych narzędzi. Lead developer, trener, prelegent.
Komentarze
Ostatnie posty
Data Science News #200
Data Science News #198
Data Science News #197