Best practices (oder Meinungen) zu functional interfaces und Methodennamen

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.