31. 5. 2015

K čemu je dobrý generický vzor Type literal

Type literal je trik s Java generikami, jehož cílem je obejít v určitých případech type erasure a dosáhnout, aby celý typ (tj. včetně generického argumentu) byl viditelný za běhu. Typy s touto vlastností se v JLS, různých tutoriálech a jiné podobné beletrii nazývají reifiable.

Trik je založen na tom, že u potomka generické třídy parametrizované pro určitý typový parametr je tento parametr čitelný pomocí reflection API, konkrétně použitím přetypování na správný interface v hierarchii pod java.lang.reflect.Type a použitím metody getActualTypeArguments. V Guavě je tento princip ztělesněn ve třídě TypeToken, ale není na knihovně nijak závislý a používám ji tedy pouze pro demonstraci. V tomto blogpostu uvádím 2 příklady z vlastní praxe, kdy použití tohoto typu vedlo – v porovnání s alternativními způsoby – k robustnějším, typově bezpečným řešením.

Příklad 1: heterogenní mapy

Heterogenní mapa je mapa, kde každá hodnota je jiného typu a tento typ zpravidla souvisí s konkrétním klíčem, se kterým je hodnota spojena. Je možné si ji představit jako typově bezpečné properties. Pokud např. konfiguraci HTTP klienta volajícího vzdálený systém, kde chceme nastavit URL a timeout, implementujeme pomocí Map<String,Object>, pak klient musí vědět, jaký typ hodnot je uložený pod jakým klíčem:

Map<String,Object> cfg = new HashMap<>(); 
...
cfg.put("url",new URL("http://example.com"));
cfg.put("timeout",60);
...
URL url = (URL)cfg.get("url");
Integer timeout = (Integer)cfg.get("timeout");

Pokud je takové zatěžování klienta nežádoucí a identifikace klíčů řetězcem je nedostatečná, je možno použít robustnější řešení: jako klíč mapy použít speciální třídu popisující klíč včetně jeho typu a obalit mapu tak, aby propustila tam i zpět jen hodnoty konzistentní s typem klíče:

class Key<T> {
    final String name;
    final Class<T> type;
    Key(String name, Class<T> type) {...}
}
... 
class HeterogenousMap { 
    Map<Key<?>,Object> entries;
    <T> T get(Key<T> key) {
        return (T)entries.get(key); 
    }
    <T> void put(Key<T> key, T value) {
        return entries.put(key,value); 
    }
...
final Key<URL> URL_KEY = new Key<URL>("url",URL.class);
final Key<Integer> TIMEOUT_KEY = new Key<Integer>("timeout",Integer.class);
...
HeterogenousMap cfg = new HeterogenousMap(); 
cfg.put(URL_KEY,new URL("http://example.com"));
cfg.put(TIMEOUT_KEY,60);
...
URL url = cfg.get(URL_KEY);
Integer timeout = cfg.get(TIMEOUT_KEY);

Nyní co když přibyde další konfigurační položka představující třeba seznam parametrů? Zápis:

final Key<List<String>> PARAMS_KEY = new Key<List<String>>("params",List<String>.class);

je ilegální. Řešení? Implementovat Key.type jako TypeToken a použít zápis

final Key<List<String>> PARAMS_KEY = new Key<List<String>>("params",new TypeToken<List<String>>(){});

Při tomto způsobu docílíme typové bezpečnosti a zároveň je ihned čitelné, jakého typu klíč je.

Příklad 2: odvození správného typu

Máme-li košatou hierarchii, TypeToken nám ulehčuje orientovat se v ní a napojit se vždy na správný typový parametr. Uvažme, že trik s vrácením správného typu je založen na výběru nultého typového argumentu, což je při doporučeném způsobu použití (zápis s pomocnou anonymní třídou) splněno. Pokud ale pravidlo nultého typového argumentu aplikujeme slepě přímo na hierarchii, obdržíme chybné výsledky. Např. pokud potomek nezachová původní typový parametr jako první. Nebo pokud se parametr hlouběji v hierarchii zkonkretizuje a ještě hlouběji se další potomek stane generickým pro parametr, který už má zcela odlišný význam. Obě situace ukazuje následující příklad:

import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;

import org.junit.Test;

import com.google.common.reflect.TypeToken;

public class TypeTokenExample {

  static class Parent<T> {
    TypeToken<T> entityToken = new TypeToken<T>(getClass()) {}; // Spravne: odchytneme typ tokenem.
    String wrongType() { // Chyba: zkoumame aktualni tridu.
      return ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0].toString();
    }
  }
 
  static class Child extends Parent<String> {} // konkretizuje predka
 
  static class Grandchild<U> extends Child {} // opet genericky argument, ale s jinym vyznamem
 
  static class Grandgrandchild extends Grandchild<BigDecimal> {} // konkretizuje pozdejsi genericky argument
 
  static class Child2<U,T> extends Parent<T> {} // puvodne prvni parametr odsouva na druhe misto
 
  static class Grandchild2 extends Child2<Byte,String> {} // konkretizuje oba parametry
 
  static void test(Parent<?> tested) {
    System.out.println("\nTESTING " + tested.getClass().getName());
    System.out.print("right = ");
    try {
      System.out.println(tested.entityToken.toString());
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
    System.out.print("wrong = ");
    try {
      System.out.println(tested.wrongType());
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }
 
  @Test public void testAll() {
    test(new Parent<Integer>());     // right: T, wrong: CCE
    test(new Child());               // right: String, wrong: String (zde je to nahodou dobre)
    test(new Grandchild<Boolean>()); // right: String, wrong: CCE
    test(new Grandgrandchild());     // right: String, wrong: BigDecimal
    test(new Grandchild2());         // right: String, wrong: Byte
  }

}

TypeToken tedy dává spolehlivější výsledky a navíc není tak ukecaný, proč ho nepoužít?

Závěr


Type literals vypadají na první pohled trochu akademicky a kdo zrovna nepracuje na nějakém metasystému (struktuře pro evidenci podoby jiné struktury), pak asi všechny jejich featury nevyužije. Přesto je patrné, že mají uplatnění i pro běžné úlohy.

Žádné komentáře:

Okomentovat