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)
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);
}
}
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).
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 enIntConsumer
et transmise à forEachRemaining (Java.util.function.IntConsumer) ; sinon l'action est adaptée à une instance deIntConsumer
, en encadrant l'argument deIntConsumer
, 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