Best practices (oder Meinungen) zu functional interfaces und Methodennamen

Im Moment hadere ich mit einigen Benennungen und Details einiger interfaces.

Angenommen, man will interfaces definieren, die im wesentlichen dem Zweck dienen, Dingen einen sprechenderen Namen zu geben. Ein Beispiel wäre, dass man verschiedenen Objekttypen wie z.B. Customer oder Product (oder auch etwas generischem) irgendeinen „Messwert“ zuordnen will. Jetzt könnte man sowas erstellen wie

interface CustomerMeausure {  double evaluateCustomer(Customer c); }
interface ProductMeausure {   double evaluateProduct(Product p); }
interface ObjectMeausure<T> { double evaluateObject(T t);  }

Die sind natürlich strukturell gleich, und mit Lambdas kann man die recht bequem mit Leben füllen. Aber mir stellen sich einige Fragen.


1. Spezifische Methodennamen?

Die wichtigste ist: Sollten auch die Methodennamen so spezifisch sein? Das wirkt irgendwie redundant und über-speziell. Man könnte auch einfach sagen

interface CustomerMeausure {  double evaluate(Customer c); }
interface ProductMeausure {   double evaluate(Product p); }
interface ObjectMeausure<T> { double evaluate(T t);  }

Und ich sehe keinen Grund, das nicht zu tun, außer dem, dass es Probleme geben kann, wenn eine Klasse mehrere solcher Interfaces implementieren will. Das ist aber ein recht „schwaches“ Argument, weil man das ja i.a. nicht muss, und so eine Klasse sowieso nicht sichtbar sein sollte. Bei der Verwendung wird die Aussagekraft meistens über den Variablennamen abgehandelt. Sowas wie customerMeasure.evaluate(c) finde ich OK - da muss nicht unbedingt customerMeasure.evaluateCustomer(c) stehen.

Gibt es weitere Argumente, die

  • dafür spechen, den Methodennamen möglichst spezifisch zu machen, oder
  • dagegen spechen, ihn so allgemein zu machen, wie in dem Beispiel?

2. Vererbung bei eigenen Interfaces

Sollte man da eine Vererbung reinziehen, und die interfaces explizit als Spezialisierungen modellieren?

Das hängt natürlich von der Antwort auf 1. ab, und teilweise von der Domäne - aber wenn man letzteres mal ignoriert: Wenn man den allgemeinen Methodennamen gewählt hat, kann man einfach sagen

interface ObjectMeausure<T> {
    double apply(T t);
}
interface CustomerMeausure  extends ObjectMeasure<Customer> {
    // No additional methods
}
interface ProductMeausure extends ObjectMeasure<Product > {
    // No additional methods
}

Wenn man die speziellen Methodennamen verwendet hat, könnte man die noch mit default-Methoden auf den allgemeineren runterbrechen:

interface ProductMeausure extends ObjectMeasure<Product > {
    default double evaluateProduct(Product p) {
        return evaluateObject(p);
    }
}

so dass man auch wieder einen Lambda-fähigen „single abstract method“-Typ hat


3. Vererbung von Standard-API-Interfaces

Sollte man solche interfaces, die strukturell gleich zu welchen aus der Standard-API sind, auch die aus der Standard-API erweitern lassen? Sowas wie die angedeutete ObjectMeasure<T> ist ja das gleiche wie eine ToDoubleFunction<T>. Also könnte man sagen

interface ObjectMeasure<T> extends ToDoubleFunction<T> {
    default double evaluateObject(T t) {
        return applyAsDouble(t);
    }
}

Die Frage nach den Methodennamen in diesem Fall ist schon fast ein Detail, im Vergleich zu der Frage: In welche Richtung wird delegiert? Sollte es vielleicht genau umgekehrt sein…? :

interface ObjectMeasure<T> extends ToDoubleFunction<T> {
    default double applyAsDouble(T t) {
        return evaluateObject(t);
    }
}

Irgendwie finde ich keine wirklich driftigen Argumente für die eine oder andere Antwort auf diese Fragen, aber irgendwie hab’ ich das Gefühl, dass das wichtig ist, und ich mir ggf. später denke: „Sh!t, das hätt’ ich genau anders machen sollen“.

Übersehe ich was?

2 Likes

Ich gehe da generell nach dem Prinzip: so wenig wie möglich, so viel wie nötig. Und dabei aber immer schön im Hinterkopf behalten: Methodennamen sind Teil der Dokumentation, also sollten die Aussagekräftig sein. Klingt nach einem krassen Paradoxon - irgendwie ^^.

Von demher hätte ich meine Methode vermutlich measure getauft. Evaluate wäre mir zu allgemein. Ich glaube, ich hätte auch das Customer oder Product dahinter - aber nur dann, wenn es keine weiteren Interfaces von dem Typ gibt. Sobald 2 oder mehr das gleiche tun, dann würde ich ein generisches erstellen und ggf. davon erben lassen.


2. Vererbung bei eigenen Interfaces

Sehe ich kein Problem darin. Aus mehreren Grünen:

  1. Es ist meistens kürzer und imho auch leserlicher.
  2. Afaik bleibt die Information über den Typ im Generic auch nach Compile-Zeit noch erhalten (nicht immer wichtig - aber könnte in speziellen Fällen wichtig werden)
  3. Du könntest angepasste JavaDocs für schreiben (je nachdem, ob man das tut ist das ein starkes oder sehr sehr schwaches Argument).

Ich würde das einfach nach Gefühl machen. Wenn ich das Bedürfnis hab ein konkreteres Interface dafür anzulegen, dann würde ich das tun.

Klingt für mich Over-Engineered. Letztendlich erschaffst du damit eine Art Contract. Die Chance ist hoch, dass du es doch bei einem einmal vergisst und dann hast du Wildwuchs drin. Und sollte mal in evaluateXXX von einem Interface extra-infos reingepackt werden, dann hast du noch nen krassen unterschied mehr dazwischen.

Du kannst also nicht mehr garantieren, dass die 2 Methoden das gleiche machen. Und generell, würde ich versuchen zu vermeiden, 2 identische Methoden anzubieten. Das sorgt immer erstmal für Verwirrung. Der Programmierer wird glauben, dass es unterschiede geben muss und nach diesen forschen.


3. Vererbung von Standard-API-Interfaces

Würde ich nicht tun. Aufgrund der unterschiedlichen Namen ist die Wahrscheinlichkeit gegeben, dass der Programmierer glauben könnte: die beiden tun unterschiedliche Dinge.

Außerdem sollte eine Vererbung immer eine IST-EIN-Relation aufweisen können. Und in diesem Fall ist die imho nicht gegeben.

Die ToDoubleFunction führt nach meinem Verständnis einen explizit gecodeten Cast aus. Dein Interface castet aber nicht. Du möchtest das Objekt in irgendeiner Form prozessieren und diesen Wert zurück geben.

1 Like

Also sofern eine Klasse nicht mehrere Interfaces implementieren soll, würde ich die aussagekräftige Interfaces derart gestalten, dass ich gleich das Standard-Interface erweitere, also ganz ohne den Umweg über ein aussagekräftiges Vater-Interface (ObjectMeasure), sofern es nicht verwendet wird. Außerdem würde ich mir - nur damit ich mir nicht Millioen aussagekräftige Namen einfallen lassen muss - ohnehin überlegen, ob es, sofern es Single-Method-Interfaces bleiben, diese eine Methode besser in die Klasse implementiere, z.B. ProductMeasure in die Klasse Product, denn dann geügt es ein generisches Interface Measure zu kreieren und dieses arbeitet dann etwa wie Comparable. Anschließend stellt sich mir dann noch die Frage, ob es auch bei mehreren Interface-Methoden noch Sinn machen würde und dan fällt mir spontan die Collections-API ein, wo es ja mehrere Methoden pro Interface gibt.
Und natürlich stellt sich überhaupt die Frage, warum „evaluate“ in spezielle Measure-Klassen ausgelagert werden soll. Das Einzige, was man damit erreicht, wäre mMn, dass die konkrete Klasse (hier Customer und Product) kürzer und damit übersichtlicher wird, aber wenn es ur um Übersicht geht, dann beschränke ich mich dabei lieber nur auf Methoden, die ich, wenn möglich auf 5 bis 10 Zeilen zusammenkürze.
Fazit: Vermutlich übersiehst du hier nur, dass es neben diesem hier noch andere „Interface-Modelle“ gibt.

Hier zeigt sich schön das Problem, das die Einführung der default Methoden IMHO darstellt.
Wenn ich sowieso eine Implementierung vor geben will, dann sollte das auch eine konkrete Klasse sein.

bye
TT

Kurz vorneweg:

Das hatte ich grob angedeutet: Es geht um sprechendere Namen und lesbaren Code. Aber wenn man das versucht, und dadurch die Generizität ggf. dramatisch eingeschränkt wird, hadere ich damit.

Um mal ein „echtes“ Beispiel zu nennen: Ich habe hier im Moment ein

public interface BiCollectionToDoubleFunction<T> 
    extends ToDoubleBiFunction<Collection<? extends T>, Collection<? extends T>>
{
    // No additional methods
}

Die Frage ist: Mit welcher Typbeschreibung will man eher hantieren:

BiCollectionToDoubleFunction<Number> fSpecific = null;
ToDoubleBiFunction<Collection<? extends Number>, Collection<? extends Number>> fGeneric = null;
    
List<Integer> list = null;
    
fSpecific.applyAsDouble(list, list);
fGeneric.applyAsDouble(list, list);

Das zweite sorgt halt irgendwann für blutende Augen. Und richtig schwierig wird das durch die Fragen zur Vererbung, siehe ganz unten…


Das gerade der Punkt der Namen es schwierig machen würde, eine klare Antwort zu geben, war klar :wink: Leute könnten evaluateCustomer, evaluate, apply oder f als Methodennamen vorschlagen, aber abgesehen von der subjektiven Präferenz scheint es schwierig zu sein, technische Argumente zu nennen.

Ein technisches wäre eben: Es sollte spezifisch sein, weil man dann weniger das Risiko eingeht, in einer Klasse nicht zwei interfaces implementieren zu können. Aber das ist, wie gesagt, eher schwach, und wenn man das genauer analysiert, wirft es weitere Detailfragen auf: Sind die Parametertypen komplett getrennt, oder erben sie voneinander? Haben die Parametertypen die gleiche Erasure?

interface ObjectMeasure<T> { double compute(T t); }
interface Customer {}
interface Product {}
interface CustomerMeasure extends ObjectMeasure<Customer> {}
interface ProductMeasure extends ObjectMeasure<Product> {}

// Geht nicht:
interface UniversalMeasure extends CustomerMeasure, ProductMeasure {}

Als JavaDoc-Nazi, und da diese Interfaces hauptsächlich dem Zweck dienen, „Dingen einen Namen zu geben“, sehe ich das als starkes Argument.

(Das passt auch noch zu ~„vermeiden, 2 identische Methoden anzubieten“).

Die mögliche Verwirrung stimmt. Das äußert sich nicht nur bei der Verwendung, sondern schon bei der Erstellung, in meiner letzten Frage - macht man dann

  • default evaluate() { return applyAsDouble(); } oder
  • default applyAsDouble() { return evaluate(); } ?

Andererseits IST das ja eine IST-EIN-Relation: Ein ProductMeasure IST ein „measure“ für ein „(Product-) object“, also ein ObjectMeasure<Product>. Und ein ObjectMeasure IST eine Funktion, die ein Objekt bekommt, und einen double-Wert liefert - also eine ToDoubleFunction.

Mehr dazu kommt im Beispiel unten…


Der wichtigste Teil an diesem Satz ist „sofern es nicht verwendet wird“. Das ist genau die Frage.


Mal ein Beispiel (compilierbar) :

(Ich weiß, dass man da mit Streams Sachen anders machen könnte, es geht nur darum, das Problem zu verdeutlichen)

Eine Klasse sollen ObjectMeasures auf ein Objekt anwenden können - und zwar eine ganze Liste davon. Und die Frage ist nun: Was ist in der Liste drin?

Man könnte der Methode eine List<ObjectMeasure<T>> übergeben. Das ist in der Methode computeAllSpecific so gemacht (mit der üblichen Wildcard-Bounds-Gymnastik).

Aber damit zieht man in diese Methode die Abhängigkeit zum eigenen ObjectMeasure-interface (und mit Abhängigkeit meine ich eine echte Dependency). Aber das, was in der Methode gemacht wird, ist eigentlich nicht domänenspezifisch: Die läuft nur über eine Liste und berechnet double-Werte für ein Objekt.

Das heißt: Eigentlich könnte man der Methode eine List<ToDoubleFunction<T>> übergeben (siehe computeAllGeneric). Aber obwohl die interfaces strukturell gleich sind, haut das nicht hin - egal wie viel WIldcard-Bounds-Magic man da drumwickelt.

interface ObjectMeasure<T> { 
    double compute(T t); 
}

class Measures
{
    void example()
    {
        List<ObjectMeasure<String>> measures = null;
        computeAllSpecific(measures, "example");
        // Geht nicht
        //computeAllGeneric(measures, "example");
    }
    
    static <T> List<Double> computeAllSpecific(
        List<? extends ObjectMeasure<? super T>> measures, T element)
    {
        List<Double> result = new ArrayList<Double>();
        for (ObjectMeasure<? super T> measure : measures)
        {
            result.add(measure.compute(element));
        }
        return result;
    }
    
    static <T> List<Double> computeAllGeneric(
        List<? extends ToDoubleFunction<? super T>> functions, T element)
    {
        List<Double> result = new ArrayList<Double>();
        for (ToDoubleFunction<? super T> function : functions)
        {
            result.add(function.applyAsDouble(element));
        }
        return result;
    }
}

Wenn man das ObjectMeasure als

interface ObjectMeasure<T> extends ToDoubleFunction<T> { 
    double compute(T t); 

    @Override
    default double applyAsDouble(T value)
    {
        return compute(value);
    }
}

definiert hätte, wäre das ganze aber problemlos möglich.


Ich denke, der Konflikt ist erkennbar:

Einerseits würde ich meine Methoden gerne generisch halten - d.h. dort sollte eine List<ToDoubleFunction<T>> übergeben werden, weil das Teil der Standard-API ist, man die Dependency zum ObjectMeasure vermeidet, und die Funktion ja eigentlich wirklich generisch ist, und nicht wissen muss, dass das, was sie da berechnet, ein „measure“ ist - solange es ein Object auf ein double abbilden kann, ist sie glücklich.

Aber damit das geht, muss man eben ObjectMeasure extends ToDoubleFunction schreiben, und spätestens da bin ich mir dann unsicher, was die „beste“ Methodenstruktur wäre…

Nun… worauf ich mitunter hinaus wollte war… wie wichtig ist es, dass ein

customerMeasure.evaluate(Product p);

möglich ist? Das ist lt. meiner persönlichen Erfahrung höchst selten so, aber mir deinem Interface-Modell durchaus möglich, wenn man mit dem Top-Level-Interface im Produktiv-Code arbeitet, wobei an die Lesbarkeit oder gar Nachvollziehbarkeit solchen Codes wohl kaum noch zu denken wäre, weil ewig selektiv gecastet werden müsste, weil generisch. Die Intention, Code lesbarer halten zu wollen, ging also somit nach hinten los, oder sehe das nur ich so? Einige Interfaces sind halt kaum dafür gemacht, im Produktiv-Code generisch verwendet werden zu können und FunctionalInterfaces scheinen dazu zu gehören.
Wären bei dem Ganzen keine Parameter involviert, wäre das Ganze viel einfacher. Man hätte dann ein Interface Measure mit einer Methode compute() und dieses kann man dann direkt in Product und Customer implementieen und Objekte beider Klassen gemischt in einer List<Measure<?>> übergeben. Andernfalls müsste man T selbst einschränken, z.B. mit einem Interface „Doubleable“, dessen Methode dann aufgerufen werden kann. Das liefe dann auch nur wieder nur aufs Erste hinaus und ist weitaus komplizierter.

War das ein Tippfehler? Nein, ein CustomerMeasure sollte NICHT auf ein Product angewendet werden können. Und es muss auch nichts gecastet werden. Es ging (nochmal ganz grob) darum, dass man sowas hat wie eine

void foo() {
    List<ProductMeasure> productMeasures = null;
    List<CustomerMeasure> customerMeasures = null;

    doSomething(productMeasures, product);
    doSomething(customerMeasures , customer);
}

private <T> void doSomething(List<WHAT> list, T element) { ...  }

Und dieses doSomething eben eigentlich in beiden Fällen das gleiche macht: Durch die Liste laufen, und von jedem Objekt, das da drin ist, die (eine!) Methode aufrufen, die das übergebene element auf ein double abbildet. Und die Frage ist (sehr auf den Punkt runterkondensiert: ) : Was steht dort bei WHAT?

Dort könnte ToDoubleFunction stehen, weil es genau das ist, was da gebraucht wird. Aber dann müssen beide *Measure-Typen von ToDoubleFunction erben…

Dan frage ich mich, wo das Problem ist. Warum funktioniert dann

import java.util.List;
import java.util.function.ToDoubleFunction;

public class FunctionTest {
  private <T> void doSomething(List<ToDoubleFunction<T>> list, T element) {
    int c = 0;
    double v = 0.0;
    for(ToDoubleFunction<T> e : list) {
      v += e.applyAsDouble(element);
      c++;
    }
    v /= c;
    return v;
  }
}

nicht? Bei mir kompiliert das ohne zu murren.

Hmja. Sicher. Aber da kann man keine List<CustomerMeasure> übergeben, außer, wenn man

//                                v---- Wildcards        ---v
private <T> void doSomething(List<? extends ToDoubleFunction<? super T>> list, T element) {

passend einsetzt, UND eben CustomerMeasure extends ToDoubleFunction<Customer> schreibt. Und letzeres ist einer der Kernpunkte der Fragen.

Was? Oh, ja, natürlich… Da müssen natürlich Wildcards hin, aber geanau so würde ich das machen. Das Interface ObjectMeasure wäre ja ohnehin überflüssig, denn es taucht ja nirgendwo mehr auf, außer im Code deiner API. Und solltest du dich dennoch dafür entscheiden, dann sollte auch doSomething nur dieses Interface verwenden, denn ein User würde dann ohnehin (normalerweise und gute Absichten vorausgesetzt) nie auf die Idee kommen mit ToDoubleFunction zu arbeiten und eigentlich brauchst du Letztere ja ohnehin nicht, wenn es etwas API-Internes werden und bleiben soll.

Jaja, die Wildcards hatte ich im anskizzierten Code auch teilweise weggelassen. Das kritische hier ist die Tatsache, dass man dann ObjectMeasure extends ToDoubleFunction machen muss.

Und natürlich könnte man sagen, dass ObjectMeasure „überflüssig“ ist, abgesehen von der Information, die im Namen steckt - und das ist schon wichtig. Nochmal das Beispiel von oben, noch etwas suggestiver:

ToDoubleBiFunction<Collection<? extends Number>, Collection<? extends Number>> f= null;
CollectionAdder<Number> g = null;

Letzteres ist halt schon eine Größenordnung lesbarer.

Die umgekehrte Argumentation, dass man dann ja nur ObjectMeasure braucht, scheitert umgekehrt gerade wieder daran, dass es dann eben eine Klasse ist, die „nur“ in meiner Lib/Anwendung existiert. Der Extremfall (wieder suggestiv)

  • Ich habe eine Utility-Lib, die das angedeutete doSomething anbieten soll
  • In einer anderen Lib hantiere ich viel mit dem, was ich gerne sprechend ObjectMeasure nennen würde (um den user nicht zu zwingen, abstrakte, nichtssagende functions rumzureichen)

Die Utility-Lib sollte natürlich ObjectMeasure nicht kennen. Und verwenden sollte man dieses Interface selbst in der Haupt-Lib nicht einfach so. Selbst für den einfachsten Fall, wo die Dinger nicht in einer Collection liegen, sollte man IMHO immer das „einfachste“ und „allgemeinste“ Interface verwenden, das man braucht, um die Aufgabe zu erfüllen. (Grob analog dazu, dass man eine Methode, die einfach nur Elemente einer Collection ausgeben soll, ja auch Iterable oder Collection übergeben würde, und nicht irgendwas spezifischeres…)

Namen sind halt immer etwas subjektives. Aber für das technische hatte ich ja noch den zweiten Punkt hinzugefügt: bedenke: der Name ist Teil der Dokumentation.

Den Punkt hatte ich auch im Kopf - aber nicht geschrieben. Natürlich sind möglichst eindeutige Namen gut - aber imho kein muss. Zumal dieser Punkt auch nochmal schwächer wird, wenn man sich ans SRP hält. Denn da, gäbe es sowas wie ein UniversalMeasure nicht. Du könntest aber ein StrategyPattern verwenden, was die einzelnen Measures kennt.

Was mir beim finden von (Methoden)namen auch oft hilft ist das Verwenden der Klasse. Wenn der Name gut ist, dann wird er dich nicht stören. Ansonsten ändert man diesen halt recht einfach mittels Refactoring-Tools.

Okay, die angedeutete doSomething-Methode ist ja nun schon in sofern spezifisch, da sie ToDoubleFunction bereits vorgibt. Wollte man nun auch noch ToDoubleBiFunction zulassen, würde eine weitere doSomething-Methode erforderlich, schon allein wegen dem Code innerhalb der Methode. Dann bekommt man aber das Problem mit „Same Type Erasure“ - an dieser Stelle hat man schon sein Kreuz mit generisch. T als Collection oder Iterable wäre hier nicht mal das Problem, denn das kann man innerhalb der Methode abfragen und die Methode aschließend sogar rekursiv aufrufen.

Nun, deine doSomething-Methode ist in dieser Hinsicht ja noch recht „offen“ (bis auf das Interface der Standard-API). So kannst du es deien Usern überlassen, ob sie Typen mit sinnvollen Namen übergeben, oder nicht - er kann sich dann sogar auch noch überlegen, ob er die spezifische List z.B. FunctionList o.ä. nennt.

Aber noch mal zurück zu ObjectMeasure… Mache es zu dem Haupt-Interface für doSomething, dann steht dir Vieles offen.

import java.util.List;
import java.util.function.ToDoubleBiFunction;
import java.util.function.ToDoubleFunction;

class Prod {
}

class Cust {
}

interface ObjectMeasure<T, R> {
	R evaluate(T value, R result);
}

interface ProdMeasure extends ObjectMeasure<Prod, Double>, ToDoubleFunction<Prod> {
	default Double evaluate(Prod p, Double v) {
		return applyAsDouble(p);
	}
}

interface CustMeasure extends ObjectMeasure<Cust, Double>, ToDoubleBiFunction<Cust, Double> {
	default Double evaluate(Cust p, Double v) {
		return applyAsDouble(p, v);
	}
}

public class FunctionTest {
	private static <T, R> R doSomething(List<? extends ObjectMeasure<? super T, ? super R>> list, T element, R result) {
		for (ObjectMeasure<? super T, ? super R> e : list) {
			 /*result = */ e.evaluate(element, result);
		}
		return result;
	}

	public static void main(String[] args) {
		Prod p = null;
		Cust c = null;
		List<ProdMeasure> lpm = null;
		List<CustMeasure> lcm = null;
		double a = doSomething(lpm, p, null);
		double b = doSomething(lcm, c, null);
	}
}

Vllt. fällt dir ja auch noch ein, wie man den Cast von R bei /* result =/* sinnvoll hinbekommt, sonst funktioniert es leider gar nicht (einfach unchecked casten, fürchte ich). :frowning:

Darum ging es nicht. ToDoubleFunction und ToDoubleBiFunction sind strukturell unterschiedlich, und haben nichts miteinander zu tun.

Vielleicht wird es so klarer:

ToDoubleFunction und ObjectMeasure sind strukturell gleich. Beide bekommen ein Objekt, und liefern ein double. Die Methode, die im interface steht, hat in beiden Fällen die gleiche Signatur (wenn auch nicht notwendigerweise den gleichen Namen).

Hier ist dieser spezielle Punkt nochmal Schritt für Schritt aufgedröselt:

import java.util.List;
import java.util.function.ToDoubleFunction;

interface ObjectMeasure<T> { 
    double compute(T t); 
}

public class MeasuresExample
{
    public static void main(String[] args)
    {
        // Der einfache Fall: Einzelne Funktionen
        ToDoubleFunction<String> f = s -> 123.0;
        ObjectMeasure<String> m = s -> 123.0;
        
        // Der richtige Typ wird übergeben - das geht
        singleF(f, "");
        singleM(m, "");

        // Wenn man "das jeweils andere" übergeben will, geht das erstmal nicht
        //singleF(m, "");
        //singleM(f, "");
        
        // Man kann aber eine method reference als "den anderen Typ" verkleiden 
        singleF(m::compute, "");
        singleM(f::applyAsDouble, "");
        
        // (Natürlich: Das GLEICHE Lambda kann an beide übergeben werden...)
        singleF(s -> 123.0, "");
        singleM(s -> 123.0, "");
        
        // Der schwierige Fall: Listen von Funktionen bzw. Measures
        List<ToDoubleFunction<String>> fs = null;
        List<ObjectMeasure<String>> ms = null;
        
        // Der richtige Typ wird übergeben - das geht
        listF(fs, "");
        listM(ms, "");
        
        // Das folgende geht aber NICHT. Und die einzige Möglichkeit, das 
        // zu erreichen, ist "ObjectMeasure extends ToDoubleFunction". 
        //listF(ms, "");
        //listM(fs, "");
        
    }
    static <T> double singleF(ToDoubleFunction<T> f, T t) { 
        return f.applyAsDouble(t);
    }
    static <T> double singleM(ObjectMeasure<T> m, T t) { 
        return m.compute(t);
    }
    
    static <T> List<Double> listM(
        List<? extends ObjectMeasure<? super T>> measures, T element)
    {
        return null;
    }
    static <T> List<Double> listF(
        List<? extends ToDoubleFunction<? super T>> measures, T element)
    {
        return null;
    }

}

Dein Vorschlag, ObjectMeasure zum „Haupt-Interface“ zu machen, trifft nicht ganz den Punkt. Es geht ja gerade darum, dass das (Hochspezifische) ObjectMeasure nicht in der allgemeinen Utility-Lib auftauchen soll und darf. Bei denem angedeuteten Code würde sich wieder genau die gleiche Frage stellen, nämlich ob

interface ObjectMeasure<T, R> {
	R evaluate(T value, R result);
}

nicht vielleicht

interface ObjectMeasure<T, R> extends Function<T, R> {
	R evaluate(T value, R result);
        // + default-Methode für 'apply' 
}

sein sollte, und doSomething dann eben kein ObjectMeasure braucht, sondern nur das in der Standard-API enthaltene Function interface.

(Dass Function und ToDoubleFunction eben NICHT strukturell gleich sind, ist dabei eher ein Detail).

Also nun weiß ich gar nicht mehr, was du überhaupt willst. Willst du nur Code über Interfaces und/oder Methodenamen lesbarer machen, was den Code weitaus komplizierter macht, was letztendlich der Intention, ihn lesbarer machen zu wollen, diametral entgegenwirkt? Ich fürchte ja, dass das kein Mensch (User) brauchen wird, denn sobald deine doSomething-Methode das Standard-API Interface anbietet, wird der User genau dieses in irgend einer Form verwenden und das vollkommen unabhängig davon, was deine API sonst noch so anbietet.
Mir ist ja nicht mal aufgefallen, dass ich ObjectMeasure die selbe Struktur, wie Function gebe, womit dieses ObjectMeasure auch vollkommen überflüssig wird. Mir ging es eigentlich nur darum, dass man nun beliebige FunctionLists an doSomething übergeben kann. Und ob da nun Interfaces mit besser lesbarem Namen und/oder Methoden übergeben werden, liegt im Ermessen des User und nicht bzw. weit weniger - deine Intention in allen Ehren - im Ermessen des API-Erstellers. Der Methodenname des Interfaces wäre, soweit ich das überblicke, ohnehin nur innerhalb von doSomething relevant.

Vielleicht hätte ich nicht mehrere Fragen mischen sollen. Aber sie hängen eben zusammen. Vielleicht ist auch aus einem anderen Grund nicht klar geworden, worauf ich raus will.


Angenommen, es soll eine Utility-Library geben. Die könnte komplett generisch sein. Sie könnte nur auf den Interfaces der Standard-API aufbauen. Im speziellen sollte sie keine dependency zu einer Library haben, wo irgendeine ominöse, höchst-spezifische CustomerMeasure rumliegt.

(Sie braucht dieses interface auch nicht, weil es strukturell gleich zu einer ToDoubleFunction ist, und das ist es, was die Library mindestens braucht. Es gibt weitere (gute!) Gründe, aber belassen wir es mal dabei.)

D.h. man könnte darin eine Methode schreiben wie

<T> void doSomething(List<? extends ToDoubleFunction<? super T>> list, T t){...}

Häßlich, durch die ? extends/ ? super-Gymnastik, aber schön, weil es dependency-frei und generisch ist. Die Priorität liegt hier auf letzterem.


Nun soll es eine andere „Application“-Library geben. Die hat zum Beispiel eine CustomerMeasure, weil sich das leichter liest, weil es die „Domäneninformation“ transportiert, und damit implizit auch dokumentiert.

(Welche Rolle da das ObjectMeasure<T> spielt, sei jetzt mal egal. Entscheidend ist: Das CustomerMeasure ist strukturell gleich zu einer ToDoubleFunction<Customer>)

Der Benutzer dieser Library braucht nicht zu schreiben

List<ToDoubleFunction<Customer>> measures = ...;

sondern er kann schreiben

List<CustomerMeasure> measures = ...;

Letzeres ist besser lesbar, und für die Application-Library steht die Lesbarkeit und einfache Benutzbarkeit im Vordergrund.

(Das oben noch eins der einfachsten Beispiele. Dass das Bedürfnis, einem Ding wie ToDoubleBiFunction<Collection<? extends Number>, Collection<? extends Number>> einen sprechenden, einfachen Namen wie NumberCollectionAdder zu geben, noch größer ist, sollte klar sein)

Und einer der Kernpunkte dieser Frage (wenn auch leider nicht der einzige) ist eben:

List<ToDoubleFunction<Customer>> functions = ...;
Utility.doSomething(functions, c);                  // Das geht

List<CustomerMeasure> measures = ...;
Utility.doSomething(measures , c);                  // Das geht NICHT!!!

Letzteres ginge nur, wenn man CustomerMeasure extends ToDoubleFunction<Customer> festlegt.

Was aber funktioniert ist

private static <T> double doSomething(List<? extends ToDoubleFunction<? extends T>> list, T element) {
  double result = 0;
  for (ToDoubleFunction<? extends T> e : list) {
    @SuppressWarnings("unchecked")
    ToDoubleFunction<T> ee = (ToDoubleFunction<T>) e;
    result += ee.applyAsDouble(element);
  }
  return result / list.size();
}

Nur leider ist hier ein uncheked Cast nötig. Wenn das kein Problem darstellt, solltest du es so machen.
Alternativ funktioniert sogar

private static <T> double doSomething(List<? extends ToDoubleFunction<T>> list, T element) {
  double result = 0;
  for (ToDoubleFunction<T> e : list) {
    result += e.applyAsDouble(element);
  }
  return result / list.size();
}

ganz ohne cast, aber vermutlich eingeschränkter.

Das ist nun nicht direkt das, worum es geht. Das super T ist OK und richtig (und extends macht da ja keinen Sinn).
Das beides geht aber davon aus, dass CustomerMeasure extends ToDoubleFunction gilt…

Das super T sorgt dafür, dass nur CustomerMeasure extends ToDoubleFunction<T> gilt. CustomerMeasure extends ToDoubleFunction<U> würde schon nicht mehr gelten. Das würde zwar eh’ nicht funktioieren, aber was ich damit sagen will, ist, dass der Platzhalter für das Generic (hier Customer) zu spezifisch ist. Deswegen funktioniert ja auch extends T oder das T alleine. Also warum sollte das keinen Sinn machen? Nur weil es bisher keine Klasse gibt, die Customer erweitert?

import java.util.List;
import java.util.function.ToDoubleFunction;

class Prod {
}

class Cust {
}

class Cust2 extends Cust {
}

interface ObjectMeasure<T> extends ToDoubleFunction<T> {
  double evaluate(T e);
}

interface ProdMeasure extends ObjectMeasure<Prod> {
  default double evaluate(Prod p) {
    return applyAsDouble(p);
  }
}

interface CustMeasure extends ObjectMeasure<Cust> {
  default double evaluate(Cust p) {
    return applyAsDouble(p);
  }
}

interface Cust2Measure extends ObjectMeasure<Cust2> {
  default double evaluate(Cust2 p) {
    return applyAsDouble(p);
  }
}

public class FunctionTest {
  private static <T> double doSomething(List<? extends ToDoubleFunction<T>> list, T element) {
    double result = 0;
    for (ToDoubleFunction<T> e : list) {
      result += e.applyAsDouble(element);
    }
    return result / list.size();
  }

  public static void main(String[] args) {
    Prod p = null;
    Cust2 c = null;
    List<ProdMeasure> lpm = null;
    List<CustMeasure> lcm = null;
    List<Cust2Measure> lcm2 = null;
    double a = doSomething(lpm, p);
    double b = doSomething(lcm, c);
    double d = doSomething(lcm2, c);
  }
}

Das extends macht aber tatsächlich keinen Sinn, weil es genau den selben Effekt wie T alleine hat… das war mir nicht bekannt.
Mir stellt sich aber nun nach wie vor die Frage, worum es dir geht. Um die Lesbarkeit und gleichzeitig um die Felibilität der Methode? Wenn das der Fall ist, ist das super definitiv zu viel.

Der Punkt, der in den Nachfragen, die bisher kamen (und sich über einen recht langen Zeitraum erstreckt haben), vielleicht untergegangen ist: An dieser Stelle geht es nicht (mehr) um die Signatur der doSomething-Methode. Wenn man die generisch schreiben will, und unabhängig von irgendwelchen domänenspezifischen *Measure-interfaces, dann ist

<T> void doSomething(List<? extends ToDoubleFunction<? super T>> list, T t){...}

gut und richtig.

(Und es ist so gesehen „das einzig Richtige“: Aussagen wie ~„es funktioniert auch mit extends oder T alleine“ irritieren mich. Das super steht da absichtlich und es erfüllt einen Zweck, und jede Alternative wäre in technischer Hinsicht „schlechter“…)

Der Punkt, von dem du jetzt zuletzt so selbstverständlich ausgegangen bist: Wenn man das so machen will, dann muss CustomerMeasure extends ToDoubleFunction gelten (ggf. auch indirekt, über das ObjectMeasure - aber das ist eine Detail). Ansonsten könnte man keine List<CustomerMeasure> and die doSomething-Funktion übergeben, weil sie ja eine List<ToDoubleFunction> erwartet.

Und wenn man sich den Schuh mit CustomerMeasure extends ToDoubleFunction anziehen will (um die doSomething-Methode so schön generisch machen zu können), dann stellt sich sofort die Frage:

Wie seht das dann aus?


1.

interface CustomerMeasure extends ToDoubleFunction<Customer> {
  // No additional methods
}

Der Aufruf von customerMeasure.applyAsDouble(customer) sähe dann komisch aus. Schöner (d.h. sprechender und selbst-dokumentierender) wäre dann sowas wie customerMeasure.evaluateCustomer(c) oder auch nur customerMeasure.evaluate(c).


2.

interface CustomerMeasure extends ToDoubleFunction<Customer> {
    double evaluateCustomer(Customer c);
}

Das ist die vermeintliche Lösung für den oben genannten „schönen“ Aufruf. Aber nicht akzeptabel, weil es dann zwei abstrakte Methoden gibt (applyAsDouble und evaluateCustomer), und man das nicht mehr als Lambda schreiben kann.


3.

interface CustomerMeasure extends ToDoubleFunction<Customer> {
    default double evaluateCustomer(Customer c) {
        return applyAsDouble(c);
    }
}

4.

interface CustomerMeasure extends ToDoubleFunction<Customer> {
    default double applyAsDouble(Customer c) {
        return evaluateCustomer(c);
    }
    double evaluateCustomer(Customer c);
}

Die sehen beide OK aus. Bei 3. ist’s eine Zeile weniger, und man baut auf der alllgemeinsten Methode auf, die es gibt - nämlich applyAsDouble. Aber wenn man das als konkrete Klasse implementiert, ist 4. irgendwie „schöner“, weil der Implementierer nur die Methode mit dem sprechenden Namen evaluateCustomer implementieren muss, und ihm fast „egal“ sein kann, dass das auch eine ToDoubleFunction mit applyAsDouble ist.

Ist eins davon „besser“?