Pourquoi Java ne peut-il pas déduire un supertype?

19

Nous savons tous que Long s'étend Number. Alors pourquoi cela ne compile-t-il pas?

Et comment définir la méthode withpour que le programme compile sans aucune conversion manuelle?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}

  • Impossible de déduire les arguments de type pour <F, R> with(F, R)
  • Le type de getNumber () du type Builder.MyInterface est Number, cela est incompatible avec le type de retour du descripteur: Long

Pour un cas d'utilisation, voir: Pourquoi le type de retour lambda n'est-il pas vérifié au moment de la compilation

jukzi
la source
Pouvez-vous poster MyInterface?
Maurice Perry
c'est déjà à l'intérieur de la classe
jukzi
Hmm, j'ai essayé <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)mais j'ai obtenu java.lang.Number cannot be converted to java.lang.Long), ce qui est surprenant car je ne vois pas où le compilateur a l'idée que la valeur de retour getterdevra être convertie returnValue.
jingx
@jukzi OK. Désolé, j'ai raté ça.
Maurice Perry
Changer Number getNumber()pour <A extends Number> A getNumber()faire fonctionner les choses. Je ne sais pas si c'est ce que tu voulais. Comme d'autres l'ont dit, le problème est que cela MyInterface::getNumberpourrait être une fonction qui revient Doublepar exemple et non Long. Votre déclaration ne permet pas au compilateur d'affiner le type de retour en fonction d'autres informations présentes. En utilisant un type de retour générique, vous autorisez le compilateur à le faire, donc cela fonctionne.
Giacomo Alzetta

Réponses:

9

Cette expression:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

peut être réécrit comme:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

Prise en compte de la signature de la méthode:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R sera déduit de Long
  • F sera Function<MyInterface, Long>

et vous passez une référence de méthode qui sera déduite comme Function<MyInterface, Number>ceci est la clé - comment le compilateur devrait-il prédire que vous voulez réellement revenir Longd'une fonction avec une telle signature? Il ne fera pas le downcasting pour vous.

Étant donné que Numberc'est une superclasse de Longet Numbern'est pas nécessairement un Long(c'est pourquoi il ne se compile pas) - vous devrez effectuer un cast explicite par vous-même:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

faire Fpour être Function<MyIinterface, Long>ou passer des arguments génériques explicitement pendant l'appel de méthode comme vous l'avez fait:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

et know Rsera vu comme Numberet le code sera compilé.

michalk
la source
C'est une fonte intéressante. Mais je cherche toujours une définition de la méthode "avec" qui fera compiler l'appelant sans aucune fonte. Quoi qu'il en soit, merci pour cette idée.
jukzi
@jukzi Vous ne pouvez pas. Peu importe comment votre withtexte est écrit. Vous avez donc le MJyInterface::getNumbertype Function<MyInterface, Number>ainsi R=Numberet vous avez également R=Longde l'autre argument (rappelez-vous que les littéraux Java ne sont pas polymorphes!). À ce stade, le compilateur s'arrête car il n'est pas toujours possible de convertir a Numberen a Long. La seule façon de résoudre ce problème est de changer MyInterfacepour utiliser <A extends Number> Numbercomme type de retour, ce qui fait que le compilateur a R=Aet puis R=Longet puisqu'il A extends Numberpeut se substituerA=Long
Giacomo Alzetta
4

La clé de votre erreur est dans la déclaration générique du type de F: F extends Function<T, R>. La déclaration qui ne fonctionne pas est: d' new Builder<MyInterface>().with(MyInterface::getNumber, 4L);abord, vous avez un nouveau Builder<MyInterface>. La déclaration de la classe implique donc T = MyInterface. Selon votre déclaration de with, Fdoit être un Function<T, R>, ce qui est un Function<MyInterface, R>dans cette situation. Par conséquent, le paramètre getterdoit prendre un MyInterfaceparamètre as (satisfait par les références de méthode MyInterface::getNumberet MyInterface::getLong) et retourner R, qui doit être du même type que le deuxième paramètre de la fonction with. Voyons maintenant si cela vaut pour tous vos cas:

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Vous pouvez "résoudre" ce problème avec les options suivantes:

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

Au-delà de ce point, c'est principalement une décision de conception pour laquelle l'option réduit la complexité du code pour votre application particulière, alors choisissez celle qui vous convient le mieux.

La raison pour laquelle vous ne pouvez pas le faire sans transtyper réside dans ce qui suit, à partir de la spécification du langage Java :

La conversion de boxe traite les expressions d'un type primitif comme des expressions d'un type de référence correspondant. Plus précisément, les neuf conversions suivantes sont appelées les conversions de boxe :

  • Du type booléen au type booléen
  • Du type octet au type octet
  • Du type court au type court
  • Du type char au type Character
  • Du type int au type Integer
  • Du type long au type Long
  • Du type float au type Float
  • Du type double au type Double
  • Du type nul au type nul

Comme vous pouvez le voir clairement, il n'y a pas de conversion de boxe implicite de long en Number, et la conversion d'élargissement de Long en Number ne peut se produire que lorsque le compilateur est sûr qu'il nécessite un nombre et non un long. Comme il existe un conflit entre la référence de méthode qui nécessite un nombre et le 4L qui fournit un long, le compilateur (pour une raison quelconque ???) ne peut pas faire le saut logique que long est un nombre et déduire que Fc'est un Function<MyInterface, Number>.

Au lieu de cela, j'ai réussi à résoudre le problème en modifiant légèrement la signature de la fonction:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

Après cette modification, les événements suivants se produisent:

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Edit:
Après y avoir passé plus de temps, il est énormément difficile d'appliquer la sécurité de type getter. Voici un exemple de travail qui utilise des méthodes de définition pour appliquer la sécurité de type d'un générateur:

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

Pourvu de la capacité de type sûr de construire un objet, nous espérons qu'à un moment donné dans le futur, nous serons en mesure de renvoyer un objet de données immuable à partir du générateur (peut-être en ajoutant une toRecord()méthode à l'interface et en spécifiant le générateur en tant que Builder<IntermediaryInterfaceType, RecordType>), vous n'avez donc même pas à vous soucier de la modification de l'objet résultant. Honnêtement, c'est une honte absolue qu'il faut tant d'efforts pour obtenir un constructeur flexible sur le terrain, mais c'est probablement impossible sans de nouvelles fonctionnalités, la génération de code ou une quantité ennuyeuse de réflexion.

Avi
la source
Merci pour tout votre travail mais je ne vois aucune amélioration pour éviter la transtypage manuel. Ma compréhension naïve est qu'un compilateur devrait pouvoir déduire (c'est-à-dire transtyper) tout ce qu'un humain peut.
jukzi
@jukzi Ma compréhension naïve est la même, mais pour une raison quelconque, cela ne fonctionne pas de cette façon. J'ai trouvé une solution de contournement qui atteint à peu près l'effet souhaité, cependant
Avi
Merci encore. Mais votre nouvelle proposition est trop large (voir stackoverflow.com/questions/58337639 ) car elle permet la compilation de ".with (MyInterface :: getNumber," I AM NOT A NUMBER ")";
jukzi
votre phrase "Le compilateur ne peut pas déduire le type de référence de méthode et 4L en même temps" est cool. Mais je veux juste que ce soit dans l'autre sens. le compilateur doit essayer Number basé sur le premier paramètre et faire un élargissement à Number du deuxième paramètre.
jukzi
1
WHAAAAAAAAAAAT? Pourquoi BiConsumer fonctionne-t-il comme prévu alors que Function ne fonctionne pas? Je ne comprends pas l'idée. J'admets que c'est exactement le type de sécurité que je voulais, mais malheureusement, cela ne fonctionne pas avec les getters. POURQUOI POURQUOI POURQUOI.
jukzi
1

Il semble que le compilateur ait utilisé la valeur 4L pour décider que R est long, et getNumber () renvoie un nombre, qui n'est pas nécessairement un long.

Mais je ne sais pas pourquoi la valeur prime sur la méthode ...

Meule
la source
0

Le compilateur Java n'est généralement pas bon pour déduire des types génériques multiples ou imbriqués ou des caractères génériques. Souvent, je ne peux pas obtenir quelque chose à compiler sans utiliser une fonction d'assistance pour capturer ou déduire certains types.

Mais, avez-vous vraiment besoin de capturer le type exact de Functionas F? Sinon, peut-être que les œuvres suivantes, et comme vous pouvez le voir, semblent également fonctionner avec des sous-types de Function.

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
        return null;
    }

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}
machfour
la source
"avec (MyInterface :: getNumber," NOT A NUMBER ")" ne devrait pas être compilé
jukzi
0

La partie la plus intéressante réside dans la différence entre ces 2 lignes, je pense:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Dans le premier cas, l' Test est explicitement Number, tout comme l' 4Lest aussi Number, pas de problème. Dans le second cas, 4Lest un Long, tout Tcomme un Long, donc votre fonction n'est pas compatible, et Java ne peut pas savoir si vous vouliez dire Numberou Long.

njzk2
la source
0

Avec la signature suivante:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

tous vos exemples sont compilés, sauf le troisième, qui requiert explicitement que la méthode ait deux variables de type.

La raison pour laquelle votre version ne fonctionne pas est que les références de méthode Java n'ont pas de type spécifique. Au lieu de cela, ils ont le type requis dans le contexte donné. Dans votre cas, Rest supposé être Longdû à la 4L, mais le getter ne peut pas avoir le type Function<MyInterface,Long>car en Java, les types génériques sont invariants dans leurs arguments.

Hoopje
la source
Votre code compilerait with( getNumber,"NO NUMBER")ce qui n'est pas souhaité. De plus, il n'est pas vrai que les génériques sont toujours invariants (voir stackoverflow.com/a/58378661/9549750 pour prouver que les génériques des setters se comportent autrement que ceux des getters)
jukzi
@jukzi Ah, ma solution a déjà été proposée par Avi. Dommage... :-). Soit dit en passant, il est vrai que nous pouvons attribuer a Thing<Cat>à une Thing<? extends Animal>variable, mais pour une covariance réelle, je m'attends à ce que a Thing<Cat>puisse être affecté à a Thing<Animal>. D'autres langages, comme Kotlin, permettent de définir des variables de type co et contravariantes.
Hoopje