web-dev-qa-db-fra.com

Casting Java interfaces fonctionnelles

Comme toujours, je regardais les sources du JDK 8 et j'ai trouvé un code très intéressant:

@Override
default void forEachRemaining(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
        forEachRemaining((IntConsumer) action);
    } 
}

La question est: comment Consumer<? super Integer> pourrait être une instance de IntConsumer? Parce qu'ils sont dans une hiérarchie différente.

J'ai créé un extrait de code similaire pour tester la diffusion:

public class InterfaceExample {
    public static void main(String[] args) {
        IntConsumer intConsumer = i -> { };
        Consumer<Integer> a = (Consumer<Integer>) intConsumer;

        a.accept(123);
    }
}

Mais il lance ClassCastException:

Exception in thread "main" 
    Java.lang.ClassCastException: 
       com.example.InterfaceExample$$Lambda$1/764977973 
     cannot be cast to 
       Java.util.function.Consumer

Vous pouvez trouver ce code sur Java.util.Spliterator.OfInt # forEachRemaining (Java.util.function.Consumer)

26
Andrii Abramov

Voyons le code ci-dessous, alors vous pouvez voir pourquoi?

class IntegerConsumer implements Consumer<Integer>, IntConsumer {
   ...
}

Toute classe peut implémenter plusieurs interfaces, l'une est Consumer<Integer> Peut-être implémente une autre est IntConsumer. Se produit parfois lorsque nous voulons adapter IntConsumer à Consumer<Integer> Et enregistrer son type d'origine (IntConsumer), alors le code ressemble à ce qui suit:

class IntConsumerAdapter implements Consumer<Integer>, IntConsumer {

    @Override
    public void accept(Integer value) {
        accept(value.intValue());
    }

    @Override
    public void accept(int value) {
        // todo
    }
}

Remarque: c'est l'utilisation de Class Adapter Design Pattern .

[~ # ~] puis [~ # ~] vous pouvez utiliser IntConsumerAdapter à la fois comme Consumer<Integer> et IntConsumer, par exemple:

Consumer<? extends Integer> consumer1 = new IntConsumerAdapter();
IntConsumer consumer2 = new IntConsumerAdapter();

Sink.OfInt Est une utilisation concrète de Class Adapter Design Pattern dans jdk-8.L'inconvénient de Sink.OfInt#accept(Integer) est clairement que JVM lancera un NullPointerException quand il accepte une valeur null, c'est pourquoi Sink est package visible.

189  interface OfInt étendSink  < Integer >,  IntConsumer  {
19 @ Remplacer
191  néant J'accepte(int valeur);
19 @ Remplacer
194  defaultnéant accepter ( Entier i) {
195  si (Tripwire.ENABLED)
196 Tripwire . trip ( getClass (), "{0} appel Sink.OfInt.accept (Integer)");
197accepter (i . intValue ());
198 }
199 }

Je l'ai trouvé pourquoi avoir besoin de lancer un Consumer<Integer> Dans un IntConsumer si passer un consommateur comme IntConsumerAdapter?

L'une des raisons est que lorsque nous utilisons un Consumer pour accepter un int, le compilateur doit le placer automatiquement dans un Integer. Et dans la méthode accept(Integer) vous devez décompresser manuellement un Integer vers un int. En d'autres termes, chaque accept(Integer) effectue 2 opérations supplémentaires pour le boxing/unboxing. Il doit améliorer les performances afin d'effectuer une vérification spéciale dans la bibliothèque d'algorithmes.

Une autre raison est de réutiliser un morceau de code. Le corps de OfInt # forEachRemaining (Consumer) est un bon exemple d'application Adapter Design Pattern pour la réutilisation OfInt # forEachRenaming (IntConsumer) .

default void forEachRemaining(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
    //   action's implementation is an example of Class Adapter Design Pattern
    //                                   |
        forEachRemaining((IntConsumer) action);
    }
    else {
    //  method reference expression is an example of Object Adapter Design Pattern
    //                                        |
        forEachRemaining((IntConsumer) action::accept);
    }
}
22
holi-java

Parce que la classe d'implémentation peut implémenter les deux interfaces.

Il est légal de convertir n'importe quel type en n'importe quel type d'interface, tant que l'objet transmis pourrait implémenter l'interface de destination . Ceci est connu à lors de la compilation comme étant faux lorsque le type source est une classe finale qui n'implémente pas l'interface, ou lorsqu'il peut être prouvé qu'il a un paramétrage de type différent qui en résulte. dans le même effacement. Au moment de l'exécution , si l'objet n'implémente pas l'interface, vous obtiendrez un ClassCastException. Vérifier avec instanceof avant d'essayer de caster vous permet d'éviter l'exception.

À partir de la spécification de langage Java, 5.5.1: Casting du type de référence :

5.5.1 Conversion de type de référence Étant donné un type de référence au moment de la compilation S (source) et un type de référence au moment de la compilation T (cible), une conversion de conversion existe de S en T si aucune erreur au moment de la compilation ne se produit en raison des règles suivantes.

...

• Si T est un type d'interface: - Si S n'est pas une classe finale (§8.1.1), alors, s'il existe un supertype X de T, et un supertype Y de S, de sorte que X et Y sont tous les deux manifestement distincts types paramétrés et que les effacements de X et Y sont les mêmes, une erreur de compilation se produit.

Sinon, le transtypage est toujours légal au moment de la compilation (car même si S n'implémente pas T, une sous-classe de S pourrait).

21
Andy Thomas

Notez que vous auriez pu trouver ce comportement sans regarder dans le code source, juste en regardant la documentation officielle de l'API , vous vous êtes lié:

Exigences de mise en œuvre:

Si l'action est une instance de IntConsumer, elle est convertie en IntConsumer et transmise à forEachRemaining (Java.util.function.IntConsumer) ; sinon l'action est adaptée à une instance de IntConsumer, en encadrant l'argument de IntConsumer, puis passée à forEachRemaining (Java.util.function.IntConsumer) .

Donc, dans les deux cas, forEachRemaining(IntConsumer) sera appelée, qui est la méthode d'implémentation réelle. Mais lorsque cela est possible, la création d'un adaptateur de boxe sera omise. La raison en est qu'un Spliterator.OfInt Est également un Spliterator<Integer>, Qui ne propose que la méthode forEachRemaining(Consumer<Integer>). Le comportement spécial permet de traiter également les instances génériques Spliterator et leurs équivalents primitifs (Spliterator.OfPrimitive), Avec une sélection automatique de la méthode la plus efficace.

Comme d'autres l'ont dit, vous pouvez implémenter plusieurs interfaces avec une classe ordinaire. En outre, vous pouvez implémenter plusieurs interfaces avec une expression lambda, si vous créez un type d'assistance, par exemple.

interface UnboxingConsumer extends IntConsumer, Consumer<Integer> {
    public default void accept(Integer t) {
        System.out.println("unboxing "+t);
        accept(t.intValue());
    }
}
public static void printAll(BaseStream<Integer,?> stream) {
    stream.spliterator().forEachRemaining((UnboxingConsumer)System.out::println);
}
public static void main(String[] args) {
    System.out.println("Stream.of(1, 2, 3):");
    printAll(Stream.of(1, 2, 3));
    System.out.println("IntStream.range(0, 3)");
    printAll(IntStream.range(0, 3));
}
Stream.of(1, 2, 3):
unboxing 1
1
unboxing 2
2
unboxing 3
3
IntStream.range(0, 3)
0
1
2
10
Holger