Implizite Parameter in Java


Scalas implizite Parameter sind ein tolles Feature: Man kann damit Typklassen simulieren, eine Art Dependency Injection implementieren, ganz allgemein Klassen schreiben, die sich mehr oder weniger intelligent an ihren „Kontext“ anpassen und vieles mehr. In Java geht das auch… na ja… fast!

Aber erst einmal von vorne. Wäre es nicht schön, eine mathematische Vektor-Klasse zu schreiben, die – statt sich auf int oder double oder so festzulegen – generisch ist? Sicher, aber dann hat man das Problem, dass die Klasse Number keinerlei Operationen auf den Zahlen definiert. Implementieren wir also unser eigenes Interface für mathematische Operationen, das hier also ähnlich wie eine Typklasse agiert:

public interface Numeric<T> {
   public T add(T a, T b);
   //u.s.w.
}

//Eine Implementierung
public class IntNumeric implements Numeric<Integer> {
    public Integer add(Integer a, Integer b) {
        return a + b;
    }
    //u.s.w.
}

Der Vorteil ist, dass das Interface auch für völlig neue Klassen wie etwa Fraction oder Complex implementiert werden kann. Hier nun eine ziemlich unsichere bis verantwortungslose generische Vector-Klasse:

public class Vector<T> {

    private final T[] data;
    private final Numeric<T> num;

    public Vector(Numeric<T> num, T... data) {
        assert data.length > 0;
        assert num != null;
        this.data = data;
        this.num = num;
    }

    public T[] getData() {
        return data;
    }
    
    public Vector<T> add(Vector<T> that) {
        assert this.data.length == that.data.length;
        T[] newData = data.clone();
        for(int i = 0; i < this.data.length; i++) {
            newData[i] = num.add(this.data[i], that.data[i]);
        }
        return new Vector<T>(newData);
    }
    
    @Override
    public String toString() {
        return Arrays.toString(data);
    }
}

So weit, so gut. Damit kann man einen Vektor für Integer, Double oder eine Complex-Klasse schreiben, solange man nur ein passendes Numeric-Objekt zur Hand hat. Aber da liegt der Hase im Pfeffer: Es ist einfach unpraktisch und langatmig, jedesmal dieses Objekt mitzugeben.

Nun könnte man Dependency Injection verwenden, oder man könnte eine Art Repository bauen, bei dem Vector nach einer geeigneten Numeric-Implementierung nachfragen kann. Es geht aber auch cooler – und gefährlicher: Was jetzt kommt ist kaum getesteter, ziemlich wackliger Code mit so einigen Haken, der nicht mehr als ein „Proof of Concept“ darstellen soll.

Schauen wir uns zuerst an, wie die Sache verwendet wird:

//Irgendwo in der Aufrufhierarchie:
@Implicit
public static Numeric<Integer> intNum = new IntNumeric();

//in derselben Klasse, oder einer von dieser direkt oder indirekt aufgerufenen Klasse
Vector<Integer> v1 = new Vector<Integer>(3,4,5);
Vector<Integer> v2 = new Vector<Integer>(6,7,8);
Vector<Integer> v3 = v1.add(v2);
System.out.println(v3);
//--> [9, 11, 13]

Offensichtlich muss es eine entsprechende Annotation geben:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Implicit {}

Und unser Vektor braucht einen zusätzlichen Konstruktor:

import static implicit.Implicits.*;

//in Vector
public Vector(T... data) {
    assert data.length > 0;
    this.data = data;
    this.num = implicitly(Numeric.class, data[0].getClass()); //<--Magie!!!
    assert num != null;
}

Dieser Code „sucht“ nach einem passenden Numeric. Das erste Argument von Implicits.imlpicitely ist die gesuchte Klasse oder das Interface. Danach folgen eventuell generische Parameter als Varargs. In unserem Fall brauchen wir einen Parameter, der denselben Typ wie unser Parameter T hat. Wir schummeln hier ganz böse und schauen uns einfach das erste Element des mitgelieferten Arrays an.

Ich sage „schummeln“, weil das ziemlich ins Auge gehen kann, nämlich dann wenn in data[0] zufällig kein Objekt der Klasse T, sondern einer Unterklasse stehen sollte. Wenn wir allerdings nur Numerics für finale Klassen wie Integer oder Double instantiieren, kann uns in dieser Hinsicht nichts passieren. Was natürlich passieren kann, ist dass kein passendes Numeric gefunden wird – und das zur Laufzeit. Dies ist der entscheidende Nachteil zu Scala: Wir geben die statische Typsicherheit an dieser Stelle auf. Löscht jemand an einem Ende des Codes einen impliziten Parameter, kann es am anderen Ende krachen.

Wie sieht nun der verkorkste Code aus, mit dem nach @Implicit-Feldern gewühlt wird? Hier ist er:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public final class Implicits {

    private Implicits() { /* do not instantiate*/ }

    public static <I> I implicitly(Class<I> baseClass, Class... paramTypes) {
        for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
            Class<?> clazz = null;
            try {
                clazz = Class.forName(ste.getClassName());
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(ex);
            }
            for (Field f : clazz.getFields()) {
                if (f.isAnnotationPresent(Implicit.class)
                        && Modifier.isStatic(f.getModifiers())
                        && baseClass.isAssignableFrom(f.getType())
                        && eq(paramTypes, getGenerics(f.getGenericType()))) {
                    try {
                        return (I) f.get(null);
                    } catch (IllegalArgumentException ex) {
                        throw new RuntimeException(ex);
                    } catch (IllegalAccessException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }
        }
        return null;
    }

    private static boolean eq(Class[] c1, Class[] c2) {
        if (c1.length != c2.length) {
            return false;
        } else {
            for (int i = 0; i < c1.length; i++) {
                if (!c1[i].equals(c2[i])) {
                    return false;
                }
            }
            return true;
        }
    }

    private static Class[] getGenerics(Type t) {
        try {
            Type[] types = ((ParameterizedType) t).getActualTypeArguments();
            Class[] classes = new Class[types.length];
            for (int i = 0; i < types.length; i++) {
                classes[i] = (Class) types[i];
            }
            return classes;
        } catch (Exception e) {
            return new Class[]{};
        }
    }
}

Das ist so ziemlich die minimalistischste Implementierung, die gerade noch läuft. Der Code hangelt sich durch den Stacktrace und sucht nach statischen Feldern, die mit @Implicit markiert sind. Falls alles (inklusive eventuell verhandener generischer Parameter) passen sollte, wird der Wert dieses Feldes zurückgeliefert. Der Match-Algorithmus ist nicht besonders ausgefeilt, er kann gerade noch zwischen einem Set<Integer> und einem Set<String> unterscheiden, tiefer geschachtelte Generics wie Set<List<Integer>> und Set<List<String>> können nicht ordentlich „aufgelöst“ werden (ehrlich gesagt habe ich mich noch nicht getraut zu schauen, was da passiert).

Fassen wir zusammen: Scalas implizite Parameter lassen sich in Java simulieren, wenn man auf Typsicherheit verzichtet, keine besonderen Ansprüche an die Performance hat, Einschränkungen bei den möglichen Typen hinnimmt und das ganze sehr, sehr vorsichtig verwendet. Wir können sogar eine Sache, die Scala nicht möglich ist: Der implizite Parameter kann zur Laufzeit ausgetauscht werden, so das nachfolgende Aufrufe von implicitly eine andere Implementierung liefern. Das ist eine interessante Möglichkeit, aber ob das wirklich eine gute Idee ist, ist natürlich eine andere Frage.

Wenn ich ein paar Unit-Tests geschrieben und dabei ein wenig Vertrauen in den Code gewonnen habe, mache ich vielleicht ein kleines Projekt daraus…

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s