Companion-Objekte in Java?

Scala verwendet statt statischer Methoden Companion-Objekte, was diverse Vorteile bietet. Insbesondere können diese Companion-Objekte selbst Interfaces implementieren oder von anderen Klassen erben. Ich überlege schon geraume Zeit, ob sich so etwas auch in Java reinmogeln ließe (natürlich wird es nie so komfortabel wie in Scala gehen).

Mal als Spiel-Bespiel eine abstrakte Zahlen-Klasse mit Selbsttyp:

public abstract class Num<N extends Num<N>> {
    public abstract N add(N n);
    public abstract N mult(N n);
}

Schön wäre es, Methoden für null und eins zu haben, die aber statisch sein müssten und deshalb hier nicht definiert werden können. Wenn wir sie in ein Companion-Objekt auslagern, können wir dagegen jetzt schon etwas damit anfangen, z.B. eine addAll -Methode implementieren:

public abstract class NumComp<N extends Num<N>> {
    public abstract N zero();
    public abstract N one();
    public N addAll(N ... ns) {
        N result = zero();
        for(N n : ns) {
            result = result.add(n);
        }
        return result;
    }
}

Dazu eine konkrete Implementierung:

public class Int extends Num<Int> {
    private final int value;
    public final static IntComp COMPANION = new IntComp();
    public Int(int value) {
        this.value = value;
    }
    @Override public Int add(Int n) {
        return new Int(value + n.value);
    }
    @Override  public Int mult(Int n) {
        return new Int(value * n.value);
    }
    @Override public String toString() {
        return String.format("Int(%d)", value);
    }
}

public class IntComp extends NumComp<Int> {
    @Override  public Int zero() {
        return new Int(0);
    }
    @Override public Int one() {
        return new Int(1);
    }
}

Die in NumComp definierte Methode funktioniert wie erwartet, und kann über Int aufgerufen werden: Int.COMPANION.addAll(new Int(19), new Int(23)) ergibt Int(42).

Wie gesagt, das ist nur eine Spielerei, um zu sehen, wie gut das funktionieren würde. Was meint ihr zu dieser Art “Parallelhierarchie”? Wo könnte es Probleme geben, was könnte man besser machen?

Ich würde die varargs-Methode addAll sehr viel lieber statisch als add(Num… values) in einer NumMath-Klasse haben…

public static Num add(Num<? extends Num<T>>... values) { if(values.length == 0) { return; } for(int n = 1; n < values; n++) { values[0].add(values[n]); } return values[0] }Der Vorteil wäre, dass man Ints, Doubles usw. beliebig miteinander vermischen könnte, vorausgesetzt, man ändert die .add-Methode entsprechend.

Normalerweise will man die Unterklassen ja nicht “mischen” können, obwohl es bei Zahlen vielleicht sinnvoll ist. Dann braucht man eigentlich auch keinen Selbsttyp. Für die Diskussion würde ich mal den allgemeinen Fall voraussetzen, dass die Untertypen getrennt bleiben sollen.

Dein Code ist übrigens inkorrekt, Num ist immutable, und du hast auch keinen null-Wert für ein leeres Array.

Ok… betrachten wir Num also als schlechtes Beispiel. Was aber wäre denn dann ein passendes Beispiel dafür? Ich hatte mal was ähnliches als ich alle Unterklassen eines Interfaces in die Form Enum pressen wollte. Dann wollte ich “switch()” und Ähnliches nutzen, aber von wegen… Das funktionierte dann doch nur wieder mit der konkreten Klasse. Kurz gesagt: Worin liegt der Sinn solcher Konstrukte (eine gemeinsame Oberklasse/Interface), wenn man die Unterklassen davon eh nicht mischen will oder kann?

Den Code habe ich da nur so hingedingst, um zu zeigen, was ich meine… wenn man Immutables hat, dann hilft immernoch

values[0] = values[0].add(values[n]);

Aber ok… das andere hätte wohl tatsächlich “return null;” heissen müssen.

Dass man z.B. sicherstellen kann, dass alle Unterklassen eine zero() oder addAll()-Methode zur Verfügung stellen, selbst wenn sie „eigentlich“ statisch wären. Sicher ist das immer noch von Namenskonventionen u.s.w. abhängig, aber immerhin gäbe es einen Kompilierfehler, wenn sich in NumComp etwas ändert, und IntComp nicht entsprechend nachzieht.

Außerdem lässt sich addAll() nicht statisch auf Ebene von Num definieren, wenn man bei einer leeren Liste kein null, sondern das Resultat der korrekten zero()-Methode der jeweiligen Implementierung nutzen will.

Warum sollte IntComp mit irgend einer Methode nachziehen müssen, wenn man ohnehin überall nur IntComp statt NumComp verwendet. Wenn man Unterklassen nicht mischen will, macht Veerbung doch gar keinen Sinn. In IntComp kann man Methoden einführen, die NumComp nicht mal anbietet. Da die Vaterklasse, solange man nicht mischt, ohnehin nur in Methoden verwendet wird, die konkrete Kindklassen verwenden, benötigt man die Vaterklasse doch nicht mehr. Irgendwo muss es also eine Verwendung der Vaterklasse geben und schon mischt man. Etwa wie beim In- und Outboxing von Number. Mir fehlt da anscheinend tatsächlich ein besserer Anwendungsfall als irgendwelche Zahlen. Ich kann da kein Weitblick für entwickeln.

Es ist (wie so oft) nicht ganz klar, worauf das abzielt (und vermutlich müßte man erst mehr über Scalas Lösung lesen, um das abschätzen und hier was sinnvolles sagen zu können). Aber diese und ähnliche Fragen habe ich mir auch schon gestellt. Teilweise in noch abstrakteren Zusammennhängen - wo zero und one dann eben die “Neutralen Elemente bezüglich Addition und Multiplikation” wären. Teilweise aber auch im allgemeineren Sinne, nämlich der Frage, wie man “traits” (oder wie man gemeinsame aber abstrakte Funktionalitäten nun auch immer nennt) “gut” abbilden kann. Man könnte ja viel über defaut methods machen…

Andere haben Lösungen in sehr allgemeinen Zusammenhängen gefunden, die wesentlich … *räusper* … pragmatischer sind: http://jscience.org/api/org/jscience/physics/amount/Amount.html#ONE


Aber etwas subjektiv: Ich finde schon den Aufruf über Int.COMPANION.addAll extrem häßlich. Je nachdem, was dort untergebracht werden soll, fände ich die Optionen nahe liegender (d.h. “hübscher”, und ggf. sogar mit teschnischen Vorteilen) :

  • Im interface eine Art getCompanion-Methode einbauen. Die hätte dann natürlich einen passenden Namen, so dass man am Ende eher sowas wie Int.math().one() schreiben würde, aber das ist ein Detail.
  • Alles, was in der COMPANION-Klasse steht, über default-Methods direkt über’s interface exponieren

Letzteres wäre natürlich nicht so schön, wenn dieses “Companion”-Objekt z.B. 10 Methoden hat, und die so ähnlich sind, dass man meistens sogar das selbe Objekt verwenden kann (mit anderen Typparametern). Die 10 Methoden dann in jedem Interface durchschleifen wirkt irgendwie stupide. Aber ich denke, das muss man auch immer abwägen, damit, was der Benutzer erwartet. Als Beispiel: Int.o+STRG+SPACE … nanu? Es gibt keine one()? Ach ja… COMPANION überall…

Also, mein aktueller Anwendungsfall sind meine Collection-Experimente in neco4j, und das Äquivalent zu zero() und one() wäre dann z.B. empty() und singleton(). Es nützt mir wenig, wenn ich diese Methoden statisch in jedem Typ anbiete, denn damit kann ich auf Top-Level immer noch keine Methoden analog zu addAll schreiben.

@anon19643277:

Mir fehlt da anscheinend tatsächlich ein besserer Anwendungsfall als irgendwelche Zahlen.

Dass das nur ein suggestives Beispiel sein sollte, um das Problem (sprachbezogen) zu verdeutlichen, erkennt man schon daran, dass addAll nicht reduce heißt :smiley:

(Nebenbei:

  • Dass zero und one hier eigentlich keine neuen Objekte zurückliefern, sondern auch Konstanten sein könnte, sehe ich als orthogonal zur Fragestellung
  • Ganz high-Level: Ich finde nicht schön, dass dort ein public static object in der Int liegt, das mit der Int-Klasse eigentlich nichts zu tun hat.

)

Ansonsten… dieser static-Charakter und die damit verbundenen Wünsche sind mir mehr oder weniger bewußt. (Ich muss mir nochmal die ganzen Fragen in Erinnerung rufen, die ich mir damals in den genannten ähnlichen Zusammenhängen gestellt habe). Aber welche Lösungsmöglichkeiten es gibt, hängt wohl davon ab, wie weit man von bestimmten Benutzungsmustern abweichen will. Wenn genau diese Funktionalität angeboten werden soll, gibt es nicht viele Alternativen zu so so einer NumComp-Klasse. Es ginge dann bestenfalls noch um die Frage, wo dieses Objekt liegt, und wie man drankommt. Sowas wie Int.COMPANION ist IMHO nicht schön - allein schon weil dort die Klasse IntComp bekannt sein muss. Sowas wie Arithmetic.forInt() läse sich IMHO leichter, und würde auch nur das Interface NumComp<Int> exponieren, und nicht die konkrete Klasse IntComp. (Aber vielleicht war das ein Detail um das es gar nicht ging…?)

Ja, habe ich gemerkt, deswegen fehlt mir ja der Weitblick. Bei dem Nächsten (Collections) wäre man ja hier da und dort schon wieder beim Mischen. Wenn z.B. in einer Methode keine konkrete benötigt wird, würde man ja die Top-Level-Klasse verwenden, wie bei den Collections, die man aus standard Java kennt.
Meine Frage zielte konkret darauf ab, was man denn mit einer Oberklasse/Interface will, wenn man sie eh nirgends verwendet, sondern nur die Konkreten einzeln. Das ist als würde man “Verwende ArrayList<> statt List<>” einem “Verwende List<> statt ArrayList<>” den Vorzug geben.