Yleiskatsaus
Javan kehittäjinä olemme saattaneet törmätä Void-tyyppiin jossain tilanteessa ja miettineet, mikä on sen tarkoitus.
Tässä nopeassa opetusohjelmassa tutustumme tähän erikoiseen luokkaan ja näemme, milloin ja miten sitä voi käyttää sekä miten sen käyttöä voi mahdollisuuksien mukaan välttää.
Mikä on Void-tyyppi
JDK 1.1:stä lähtien Java tarjoaa meille Void-tyypin. Sen tarkoitus on yksinkertaisesti esittää void-paluutyyppi luokkana ja sisältää julkisen arvon Class<Void>. Se ei ole instantioitavissa, koska sen ainoa konstruktori on private.
Siten ainoa arvo, jonka voimme antaa Void-muuttujalle, on null. Se voi tuntua hieman hyödyttömältä, mutta katsomme nyt, milloin ja miten tätä tyyppiä käytetään.
Käyttökohteet
On joitakin tilanteita, joissa Void-tyypin käyttäminen voi olla mielenkiintoista.
3.1. Void-tyypin käyttö. Heijastaminen
Esimerkiksi voisimme käyttää sitä tehdessämme heijastusta. Minkä tahansa void-metodin paluutyyppi vastaa nimittäin aiemmin mainittua Class<Void>-arvoa sisältävää Void.TYPE-muuttujaa.
Ajatellaanpa yksinkertaista Calculator-luokkaa:
public class Calculator { private int result = 0; public int add(int number) { return result += number; } public int sub(int number) { return result -= number; } public void clear() { result = 0; } public void print() { System.out.println(result); }}
Jotkut metodit palauttavat kokonaisluvun, jotkin metodit eivät palauta mitään. Oletetaan nyt, että meidän on haettava heijastamalla kaikki metodit, jotka eivät palauta mitään tulosta. Saamme tämän aikaan käyttämällä Void.TYPE-muuttujaa:
@Testvoid givenCalculator_whenGettingVoidMethodsByReflection_thenOnlyClearAndPrint() { Method calculatorMethods = Calculator.class.getDeclaredMethods(); List<Method> calculatorVoidMethods = Arrays.stream(calculatorMethods) .filter(method -> method.getReturnType().equals(Void.TYPE)) .collect(Collectors.toList()); assertThat(calculatorVoidMethods) .allMatch(method -> Arrays.asList("clear", "print").contains(method.getName()));}
Kuten näemme, vain clear()- ja print()-metodit on haettu.
3.2. Generics
Toinen Void-tyypin käyttö on geneerisissä luokissa. Oletetaan, että kutsumme metodia, joka vaatii Callable-parametrin:
public class Defer { public static <V> V defer(Callable<V> callable) throws Exception { return callable.call(); }}
Mutta välitettävän Callablen ei tarvitse palauttaa mitään. Siksi voimme välittää Callable<Void>:
@Testvoid givenVoidCallable_whenDiffer_thenReturnNull() throws Exception { Callable<Void> callable = new Callable<Void>() { @Override public Void call() { System.out.println("Hello!"); return null; } }; assertThat(Defer.defer(callable)).isNull();}
Olisimme voineet joko käyttää satunnaista tyyppiä (esim. Callable<Integer>) ja palauttaa nollan tai ei mitään tyyppiä lainkaan (Callable), mutta Voidin käyttäminen ilmaisee aikomuksemme selvästi.
Voidaan soveltaa tätä menetelmää myös lambdoihin. Itse asiassa Callablemme olisi voitu kirjoittaa lambdaksi. Kuvitellaanpa metodi, joka vaatii funktion, mutta haluamme käyttää funktiota, joka ei palauta mitään. Silloin meidän täytyy vain laittaa se palauttamaan Void:
public static <T, R> R defer(Function<T, R> function, T arg) { return function.apply(arg);}
@Testvoid givenVoidFunction_whenDiffer_thenReturnNull() { Function<String, Void> function = s -> { System.out.println("Hello " + s + "!"); return null; }; assertThat(Defer.defer(function, "World")).isNull();}
Miten välttää sen käyttöä?
Nyt olemme nähneet joitakin Void-tyypin käyttötapoja. Vaikka ensimmäinen käyttötapa on täysin ok, voimme kuitenkin välttää Voidin käyttöä geneerisissä tapauksissa, jos mahdollista. Paluutyypin kohtaaminen, joka edustaa tuloksen puuttumista ja voi sisältää vain nollan, voi nimittäin olla hankalaa.
Katsomme nyt, miten näitä tilanteita voidaan välttää. Tarkastellaan ensin metodiamme, jossa on Callable-parametri. Välttääksemme Callable<Void>:n käytön, voisimme sen sijaan tarjota toista metodia, joka ottaa parametrin Runnable:
public static void defer(Runnable runnable) { runnable.run();}
Voimme siis luovuttaa sille Runnable:n, joka ei palauta mitään arvoa, ja siten päästä eroon hyödyttömästä return null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Mutta entä sitten, mitä tapahtuu, jos Defer-luokkaa ei olekaan meidän muokattavana? Silloin voimme joko pitäytyä Callable<Void> -vaihtoehdossa tai luoda toisen luokan, joka ottaa Runnable-luokan ja siirtää kutsun Defer-luokalle:
public class MyOwnDefer { public static void defer(Runnable runnable) throws Exception { Defer.defer(new Callable<Void>() { @Override public Void call() { runnable.run(); return null; } }); }}
Toimimalla näin kapseloimme hankalan osan lopullisesti omaan metodiimme, jolloin tulevat kehittäjät voivat käyttää yksinkertaisempaa API:ta.
Sama onnistuu tietysti myös Functionille. Esimerkissämme Function ei palauta mitään, joten voimme tarjota toisen metodin, joka ottaa sen sijaan Consumerin:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
Entä jos funktiomme ei ota mitään parametria? Voimme joko käyttää Runnablea tai luoda oman funktionaalisen rajapinnan (jos se tuntuu selkeämmältä):
public interface Action { void execute();}
Tällöin ylikuormitamme jälleen defer()-metodin:
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Conclusion
Tässä lyhyessä artikkelissa käsiteltiin Java Void-luokka. Näimme, mikä oli sen tarkoitus ja miten sitä käytetään. Opimme myös joitakin vaihtoehtoja sen käytölle.
Tuttuun tapaan tämän artikkelin koko koodi löytyy GitHubistamme.
Aloita Spring 5:n ja Spring Boot 2:n käyttö Opi Spring -kurssin avulla:
>> KATSO KURSSI