Nous pouvons simuler l'implémentation des méthodes d'extension C # en Java en utilisant l'implémentation de méthode par défaut disponible depuis Java 8. Nous commençons par définir une interface qui nous permettra d'accéder à l'objet support via une méthode base (), comme ceci:
public interface Extension<T> {
default T base() {
return null;
}
}
Nous retournons null car les interfaces ne peuvent pas avoir d'état, mais cela doit être corrigé ultérieurement via un proxy.
Le développeur d'extensions devrait étendre cette interface par une nouvelle interface contenant des méthodes d'extension. Disons que nous voulons ajouter une interface forEach consumer sur List:
public interface ListExtension<T> extends Extension<List<T>> {
default void foreach(Consumer<T> consumer) {
for (T item : base()) {
consumer.accept(item);
}
}
}
Comme nous étendons l'interface d'extension, nous pouvons appeler la méthode base () dans notre méthode d'extension pour accéder à l'objet de support auquel nous nous attachons.
L'interface d'extension doit avoir une méthode de fabrique qui créera une extension d'un objet de support donné:
public interface Extension<T> {
...
static <E extends Extension<T>, T> E create(Class<E> type, T instance) {
if (type.isInterface()) {
ExtensionHandler<T> handler = new ExtensionHandler<T>(instance);
List<Class<?>> interfaces = new ArrayList<Class<?>>();
interfaces.add(type);
Class<?> baseType = type.getSuperclass();
while (baseType != null && baseType.isInterface()) {
interfaces.add(baseType);
baseType = baseType.getSuperclass();
}
Object proxy = Proxy.newProxyInstance(
Extension.class.getClassLoader(),
interfaces.toArray(new Class<?>[interfaces.size()]),
handler);
return type.cast(proxy);
} else {
return null;
}
}
}
Nous créons un proxy qui implémente l'interface d'extension et toute l'interface implémentée par le type de l'objet de support. Le gestionnaire d'appel donné au proxy distribuerait tous les appels à l'objet de support, à l'exception de la méthode "base", qui doit renvoyer l'objet de support, sinon son implémentation par défaut renvoie null:
public class ExtensionHandler<T> implements InvocationHandler {
private T instance;
private ExtensionHandler(T instance) {
this.instance = instance;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if ("base".equals(method.getName())
&& method.getParameterCount() == 0) {
return instance;
} else {
Class<?> type = method.getDeclaringClass();
MethodHandles.Lookup lookup = MethodHandles.lookup()
.in(type);
Field allowedModesField = lookup.getClass().getDeclaredField("allowedModes");
makeFieldModifiable(allowedModesField);
allowedModesField.set(lookup, -1);
return lookup
.unreflectSpecial(method, type)
.bindTo(proxy)
.invokeWithArguments(args);
}
}
private static void makeFieldModifiable(Field field) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField
.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
}
Ensuite, nous pouvons utiliser la méthode Extension.create () pour attacher l'interface contenant la méthode d'extension à l'objet de support. Le résultat est un objet qui peut être transtypé vers l'interface d'extension par laquelle nous pouvons toujours accéder à l'objet de support appelant la méthode base (). Une fois la référence castée vers l'interface d'extension, nous pouvons maintenant appeler en toute sécurité les méthodes d'extension qui peuvent avoir accès à l'objet de support, de sorte que nous pouvons maintenant attacher de nouvelles méthodes à l'objet existant, mais pas à son type de définition:
public class Program {
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c");
ListExtension<String> listExtension = Extension.create(ListExtension.class, list);
listExtension.foreach(System.out::println);
}
}
C'est donc une façon de simuler la capacité d'étendre des objets en Java en leur ajoutant de nouveaux contrats, ce qui nous permet d'appeler des méthodes supplémentaires sur les objets donnés.
Vous trouverez ci-dessous le code de l'interface d'extension:
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
public interface Extension<T> {
public class ExtensionHandler<T> implements InvocationHandler {
private T instance;
private ExtensionHandler(T instance) {
this.instance = instance;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if ("base".equals(method.getName())
&& method.getParameterCount() == 0) {
return instance;
} else {
Class<?> type = method.getDeclaringClass();
MethodHandles.Lookup lookup = MethodHandles.lookup()
.in(type);
Field allowedModesField = lookup.getClass().getDeclaredField("allowedModes");
makeFieldModifiable(allowedModesField);
allowedModesField.set(lookup, -1);
return lookup
.unreflectSpecial(method, type)
.bindTo(proxy)
.invokeWithArguments(args);
}
}
private static void makeFieldModifiable(Field field) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
}
default T base() {
return null;
}
static <E extends Extension<T>, T> E create(Class<E> type, T instance) {
if (type.isInterface()) {
ExtensionHandler<T> handler = new ExtensionHandler<T>(instance);
List<Class<?>> interfaces = new ArrayList<Class<?>>();
interfaces.add(type);
Class<?> baseType = type.getSuperclass();
while (baseType != null && baseType.isInterface()) {
interfaces.add(baseType);
baseType = baseType.getSuperclass();
}
Object proxy = Proxy.newProxyInstance(
Extension.class.getClassLoader(),
interfaces.toArray(new Class<?>[interfaces.size()]),
handler);
return type.cast(proxy);
} else {
return null;
}
}
}