Best practices (oder Meinungen) zu functional interfaces und Methodennamen

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“?

Also ich verstehe nicht, welchen Sinn dieses super für diesen Zweck erfüllen soll, denn es wirkt dem Ziel entgegen und von daher kann es kaum gut ubd richtig sein (mM) In der doSomething kann eh nur applyAsDouble aufgerufen werden, also was solls. Was man aber für konkrete Implementationen noch machen kann, wenn beide Methoden haben will:

interface ObjectMeasure<T> {
  double evaluate(T element);
}
interface CustomerMeasure extends ToDoubleFunction<Customer>, ObjectMeasure<Customer> {
  default double applyAsDouble(Customer c) {
    return evaluate(c);
  }
}

Jetzt kannst du auch „? super T“ schreiben, extends und T alleine funktionieren aber nach wie vor auch. Nur hat das den „Nachteil“ dass ObjectMeasure und ToDoubleFunction nicht mehr zwangsläufig zusammen implementiert werden, was dahingehend wichtig wäre, wenn sich User der Lib darauf einschießen.

Nun, das driftet wieder von der eigentlichen Frage weg. Und das super erfüllt in diesem Fall so gesehen keinen Zweck, weil in dem dummy-Beispiel das <T> ja ohnehin „frei“ bei der Methode steht, und die Typinferenz sich da schon den passenden Typ sucht. Aber allgemein gilt eben die PECS-Regel: „Producer:extends, Consumer:super

Das folgende Beispiel wirkt im ersten Moment vielleicht etwas konstruiert (hmja… vielleicht weil es schnell konstruiert ist :confused: ). Aber wenn man da nicht konsequent darauf achtet, pflastert man sich seinen Code halt mit Funktionen zu, die eigentlich mit bestimmten Argumenten aufgerufen werden könnten, aber nicht aufgerufen werden können, weil irgendwo ein super oder extends fehlt…

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

public class SuperExample<T>
{
    public static void main(String[] args)
    {
        List<SpecialCustomer> specialCustomers = null;
        List<CustomerMeasure> customerMeasures = null;
        
        SuperExample<SpecialCustomer> c = new SuperExample<SpecialCustomer>();
        
        //c.doSomething(specialCustomers, customerMeasures); // Nope!
        c.doSomethingWithSuper(specialCustomers, customerMeasures); // Yes!
    }
    
    void doSomething(
        List<? extends T> customers,
        List<? extends ToDoubleFunction<T>> functions)
    {
    }
    void doSomethingWithSuper(
        List<? extends T> customers,
        List<? extends ToDoubleFunction<? super T>> functions)
    {
    }
    
}

class Customer
{
}

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

class SpecialCustomer extends Customer
{
}

interface SpecialCustomerMeasure extends ToDoubleFunction<SpecialCustomer>
{
    default double evaluate(SpecialCustomer p)
    {
        return applyAsDouble(p);
    }
}

Zur eigentlichen Frage: Ich bin jetzt bei dem Ansatz von Measure extends ToDoubleFunction, mit einer default-Implementierung von applyAsDouble, die an die compute-Methode des Measure-interfaces delegiert. Ein Beispiel:

/**
 * Interface for classes that compute a measure from two {@link Cluster}
 * instances
 *
 * @param <T> The element type
 */
public interface BinaryClusterMeasure<T> extends 
    ToDoubleBiFunction<Cluster<? extends T>, Cluster<? extends T>> 
{
    /**
     * Compute the measure for the given {@link Cluster} instances
     * 
     * @param cluster0 The first {@link Cluster}
     * @param cluster1 The second {@link Cluster}
     * @return The measure
     */
    double compute(Cluster<? extends T> cluster0, Cluster<? extends T> cluster1);
    
    @Override
    default double applyAsDouble(Cluster<? extends T> t, Cluster<? extends T> u)
    {
        return compute(t, u);
    }
}

Aber wo genau gilt diese Regel? Ich würde ja sagen, dass sie erst im Code der Methode (doSomething) zum Tragen kommt. Die „Methodensignatur“ selbst ist ja noch nicht der Consumer. Das Problem ist also, dass wenn da ? super T in der Signatur auftaucht, im Methodencode nicht mehr viel nach dem passenden Typ gesucht werden kann.

…und nun musst du in jedem XxxClusterMeasure explizit die Methode compute definieren… Hier fuktioniert auch

interface ClusterMeasure<T> {
  double compute(Cluster<? extends T> cluster0, Cluster<? extends T> cluster1);
}
public interface BinaryClusterMeasure<T> extends
  ToDoubleBiFunction<Cluster<? extends T>, Cluster<? extends T>>,
  ClusterMeasure<T>
{
  @Override
  default double applyAsDouble(Cluster<? extends T> t, Cluster<? extends T> u) {
    return compute(t, u);
  }
}

Aber vermutlich sollte BinaryClusterMeasure bereits das sein, was nun ClusterMeasure ist. Das ToDoubleBiFunction-Interface jedenfalls macht erst wieder im konkreteren Interface Sinn, analog zu vorhergehenden Beispielen also etwa

interface CustomerBinaryClusterMeasure extends
  ToDoubleBiFunction<Cluster<? extends Customer>, Cluster<? extends Customer>>,
  ClusterMeasure<Customer>{
...
}

Allerdings gilt dies nach wie vor nur, wenn in Methoden, an die Instanzen dieses Interfaces übergeben werden sollen, warum auch immer, super statt extends oder gar keinem Zusatz steht.

Nun, wenn die ToDoubleFunction dort ein T übergeben bekommt, dann ist sie eben der „consumer“, und darum sollten die Anforderungen an den Typ durch das ? super gelockert werden. Das bietet dann einfach mehr Möglichkeiten beim Aufruf.

Jein. Technisch geht das. Schön. Das ist mir aber egal. Wenn dann jemand eine List<ClusterMeasure> an eine Funktion übergeben will, die eine List<ToDoubleBiFunction> erwartet, dann geht das eben nicht mehr. Aber das sollte gehen, weil beide Typen (also sowohl ClusterMeasure als auch ToDoubleBiFunction) dieselbe Struktur haben:

//  Die beiden ...                   ... sind strukturell GLEICH:
ClusterMeasure<T> c                                              = (c0, c1) -> 0.0;
ToDoubleBiFunction<Cluster<? extends T>, Cluster<? extends T>> f = (c0, c1) -> 0.0;

Ersteres liest sich halt schöner. Aber damit man diese strukturelle Gleichheit ausnutzen kann, muss man eben extends ToDoubleBiFunction<Cluster<? extends T>, Cluster<? extends T>> direkt an das interface schreiben, das diese strukturelle Gleichheit (durch die einzelne Methode, die es enthält) vorgibt.

Hast du in dem ganzen Faden etwa och gar nicht registriert, dass genau das Gegenteil der Fall ist? „? super T“ im Parameter der Methode schränkte bisher nur ein. Mehr möglich war mit „T“ alleine und mit „? extends T“.

Ähm doch, es geht. Es geht dann sogar bei Methoden, die „? super T“ erwarten, statt nur „T“ oder „? extends T“ - das ist der Punkt. Die strukturelle Gleichheit der Interfaces interessiert hier die VM gar nicht, sie kennt sie ja bis zum Aufruf der Methoden nicht mal, es ist der Compiler, der den User darauf hinweist, dass etwas nicht fuktionieren wird.

In dem Beitrag oben habe ich ein (vollständiges, compilierbares) Bespiel gezeigt, wo man sieht, dass ? super einen Aufruf erlaubt, der ohne das ? super eben nicht möglich wäre. The proof is in the pudding.

Ähm nein, es geht nicht. Das, was du in diesem Beitrag unter „Hier fuktioniert auch“ geschrieben hast, ist hier mal rein-ge-copied-and-pasted, und man sieht, dass es nicht geht.

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

class Cluster<T>
{
    
}
interface ClusterMeasure<T>
{
    double compute(Cluster<? extends T> cluster0,
        Cluster<? extends T> cluster1);
}

interface BinaryClusterMeasure<T>
    extends ToDoubleBiFunction<Cluster<? extends T>, Cluster<? extends T>>,
    ClusterMeasure<T>
{
    @Override
    default double applyAsDouble(Cluster<? extends T> t, Cluster<? extends T> u)
    {
        return compute(t, u);
    }
}

public class Example
{
    public static void main(String[] args)
    {
        List<ToDoubleBiFunction<Cluster<Integer>, Cluster<Integer>>> listA = null;
        List<ClusterMeasure<Integer>> listB = null;
        
        example(listA); // Yep
        //example(listB); // Nope
    }
    
    private static void example(
        List<ToDoubleBiFunction<Cluster<Integer>, Cluster<Integer>>> list)
    {
        
    }
}

(Ich finde es etwas schade, dass diese Diskussion notwendig ist, ganz nebenbei gesagt)

Also ClusterMeasure war auch nicht das konkretere Interface. Das konkretere Interface war BinaryClusterMeasure, vllt. versuchst du es mal damit. Und ja, es ist schade, dass diese Diskussion notwendig ist. Aber warum ist sie überhaupt notwendig? I deinem „Pudding“ hattest du eine ObjectMeasure, die ToDoubleFunction erweiterte und wenn ein konkreteres Interface (z.B. CustomMeasure) dieses ObjectMeasure erweiterte, konnte eine Methode, die ToDoubleFunction<? super T> erwartete mit diesem CustomMeasure nichts mehr anfangen. Selbiges traf auch auf das zweite Beispiel mit ToDoubleBiFunction zu. Dafür habe ich genau 2 brauchbare Möglichkeiten genannt:

  1. Das konkretere Interface (z.B. CustomMeasure) implementiert das API-Interface (z.B. ToDoubleFunction<T>) und nicht das Lib-Interface (ObjectMeasure<T>)
  2. Man reduziert den Methodenparameter um "? super " auf T

Zweitens ist natürlich undenkbar, wenn man nicht selbst Autor der Lib/API ist, die diese Methode anbietet, dann muss man auf 1. zurückgreifen. Wichtig ist, dass wenn Interface<?super T> wie hier verwendet wird, die übergebene Klasse Interface<T> selbst erweitern muss und nicht über ein Interface, welches seinerseits Interface<T> erweitert. Mehr gibt es von meiner Seite aus dazu nicht zu sagen, also mach was draus oder lass es.