Java equivalente a los métodos de extensión C #

176

Estoy buscando implementar una funcionalidad en una lista de objetos como lo haría en C # usando un método de extensión.

Algo como esto:

List<DataObject> list;
// ... List initialization.
list.getData(id);

¿Cómo hago eso en Java?

Fabio Milheiro
fuente
8
Comprueba este: github.com/nicholas22/jpropel, ejemplo: new String [] {"james", "john", "john", "eddie"} .where (comienza con ("j")). Distinct (); Utiliza lombok-pg que proporciona la bondad del método de extensión.
NT_
66
Microsoft definitivamente acertó cuando permitieron extensiones. Subclasificar para agregar nueva funcionalidad no funciona si necesito que la función en una clase me sea devuelta en otro lugar. Como agregar métodos a String y Date.
tggagne
3
es decir, java.lang.String es una clase final, por lo que no puede ampliarla. El uso de métodos estáticos es una forma, pero a veces muestra código ilegible. Creo que C # dejó una era como una computadora lang. Métodos de extensión, clases parciales, LINQ, etc.
Davut Gürbüz
77
@Roadrunner, rofl! La mejor respuesta a una característica de idioma faltante es que dicha característica de idioma faltante es malvada y no deseada. Esto es conocido.
Kirk Woll
66
Los métodos de extensión no son "malvados". Mejoran enormemente la legibilidad del código. Solo una más de las muchas malas decisiones de diseño en Java.
csauve

Respuestas:

196

Java no admite métodos de extensión.

En cambio, puede hacer un método estático regular o escribir su propia clase.

SLaks
fuente
63
Estoy malcriado después de usar métodos de extensión, pero los métodos estáticos también funcionarán.
bbqchickenrobot
31
Pero la sintaxis es muy agradable y hace que el programa sea más fácil de entender :) También me gusta cómo Ruby te permite hacer casi lo mismo, excepto que puedes modificar las clases integradas y agregar nuevos métodos.
conocidoasilya
18
@Ken: Sí, ¡y ese es el punto! ¿Por qué escribe en Java y no directamente en el código de bytes JVM? ¿No es "solo una cuestión de sintaxis"?
Fyodor Soikin
30
Los métodos de extensión pueden hacer que el código sea mucho más elegante en comparación con los métodos estáticos adicionales de alguna otra clase. Casi todos los lenguajes más modernos permiten algún tipo de extensión de clase existente: C #, php, Objective-C, JavaScript. Java seguramente muestra su edad aquí. Imagine que desea escribir un JSONObject en el disco. ¿Llamas a jsonobj.writeToDisk () o someunrelatedclass.writeToDisk (jsonobj)?
woens
8
Las razones para odiar a Java siguen creciendo. Y dejé de buscarlos hace un par de años ......
John Demetriou
54

Los métodos de extensión no son solo métodos estáticos y no solo azúcar de sintaxis de conveniencia, de hecho son una herramienta bastante poderosa. Lo principal es la capacidad de anular diferentes métodos basados ​​en la creación de instancias de diferentes parámetros genéricos. Esto es similar a las clases de tipo de Haskell y, de hecho, parece que están en C # para admitir las mónadas de C # (es decir, LINQ). Incluso dejando caer la sintaxis de LINQ, todavía no conozco ninguna forma de implementar interfaces similares en Java.

Y no creo que sea posible implementarlos en Java, debido a la semántica de borrado tipo Java de los parámetros genéricos.

user1686250
fuente
También le permiten heredar múltiples comportamientos (sin polimorfismo). Puede implementar múltiples interfaces, y con eso vienen sus métodos de extensión. También le permiten implementar el comportamiento que desea adjuntar a un tipo sin tenerlo asociado globalmente con el tipo en todo el sistema.
Neologismo inteligente
25
Toda esta respuesta es incorrecta. Los métodos de extensión en C # son simplemente azúcar sintáctica que el compilador reorganiza un poco para mover el objetivo de la llamada al primer argumento del método estático. No puede anular los métodos existentes. No hay requisito de que un método de extensión sea una mónada. Es, literalmente, una forma más conveniente de llamar a un método estático, que da la apariencia de agregar métodos de instancia a una clase. Lea esto si está de acuerdo con esta respuesta
Matt Klein
3
bueno, en este caso, defina qué es el azúcar de sintaxis, yo llamaría a un azúcar de sintaxis como sintaxis macro interna, para que el compilador de métodos de extensión al menos busque la clase estática que el método de extensión encuentra para sustituir. No hay nada en la respuesta sobre el método que debe ser mónada que no tiene sentido. Además, puede usarlo para sobrecargar, pero no es una función de métodos de extensión, es una sobrecarga basada en un tipo de parámetro simple, de la misma manera que funcionaría si se llama al método directamente, y no funcionará en muchos casos interesantes en Java debido a la eliminación de argumentos de tipo genérico.
user1686250
1
@ user1686250 Es posible implementar en Java (por "Java" supongo que te refieres a bytecode que se ejecuta en un JVM) ... Kotlin, que compila a bytecode tiene extensiones. Es solo azúcar sintáctico sobre métodos estáticos. Puede usar el descompilador en IntelliJ para ver cómo se ve el Java equivalente.
Jeffrey Blattman
@ user1686250 ¿Podría desarrollar lo que está escribiendo sobre genéricos (o dar un enlace) porque no entiendo absolutamente nada sobre los genéricos. ¿De qué otra manera está relacionado con los métodos estáticos habituales?
C.Champagne
10

Técnicamente, la extensión C # no tiene equivalente en Java. Pero si desea implementar tales funciones para un código más limpio y fácil de mantener, debe usar el marco de Manifold.

package extensions.java.lang.String;

import manifold.ext.api.*;

@Extension
public class MyStringExtension {

  public static void print(@This String thiz) {
    System.out.println(thiz);
  }

  @Extension
  public static String lineSeparator() {
    return System.lineSeparator();
  }
}
M.Laida
fuente
7

El lenguaje XTend , que es un superconjunto de Java y se compila en el código fuente 1 de Java  , lo admite.

Sam Keays
fuente
Cuando ese código que no es Java se compila en Java, ¿tiene un método de extensión? ¿O el código Java es solo un método estático?
Fabio Milheiro
@Bomboca Como otros han señalado, Java no tiene métodos de extensión. Entonces, el código XTend, compilado en Java, de alguna manera no crea un método de extensión Java. Pero si trabaja en XTend exclusivamente, no lo notará ni le importará. Pero, para responder a su pregunta, tampoco tiene necesariamente un método estático. El autor principal de XTend tiene una entrada de blog sobre esto en blog.efftinge.de/2011/11/…
Erick G. Hagstrom
Sí, no sé por qué no pensé eso también. ¡Gracias!
Fabio Milheiro
@Sam Gracias por presentarme a XTend, nunca había oído hablar de él.
jpaugh
7

Manifold proporciona Java con métodos de extensión de estilo C # y varias otras características. A diferencia de otras herramientas, colector no tiene limitaciones y no sufren de problemas con los genéricos, lambdas, etc. IDE colector proporciona varias otras características tales como F # al estilo de tipos personalizados , de estilo mecanografiado las interfaces estructurales y de estilo Javascript tipos expando .

Además, IntelliJ proporciona soporte integral para Manifold a través del complemento Manifold .

Manifold es un proyecto de código abierto disponible en github .

Scott
fuente
6

Otra opción es usar las clases ForwardingXXX de la biblioteca google-guava.

Aravind Yarram
fuente
5

Java no tiene esa característica. En su lugar, puede crear una subclase regular de la implementación de su lista o crear una clase interna anónima:

List<String> list = new ArrayList<String>() {
   public String getData() {
       return ""; // add your implementation here. 
   }
};

El problema es llamar a este método. Puedes hacerlo "en su lugar":

new ArrayList<String>() {
   public String getData() {
       return ""; // add your implementation here. 
   }
}.getData();
AlexR
fuente
112
Eso es completamente inútil.
SLaks
2
@Slaks: ¿Por qué exactamente? Esta es una "escribe tu propia clase" sugerida por ti mismo.
Goran Jovic
23
@Goran: Todo lo que le permite hacer es definir un método, luego llamarlo inmediatamente, una vez .
SLaks
3
@Slaks: Muy bien, punto tomado. En comparación con esa solución limitada, escribir una clase con nombre sería mejor.
Goran Jovic
Hay una gran diferencia entre los métodos de extensión de C # y las clases anónimas de Java. En C #, un método de extensión es azúcar sintáctico para lo que realmente es solo un método estático. El IDE y el compilador hacen que un método de extensión parezca un método de instancia de la clase extendida. (Nota: "extendido" en este contexto no significa "heredado" como lo haría normalmente en Java.)
HairOfTheDog
4

Parece que hay una pequeña posibilidad de que los métodos de defensa (es decir, los métodos predeterminados) puedan ingresar a Java 8. Sin embargo, hasta donde yo los entiendo, solo permiten que el autor de un lo interfaceextienda retroactivamente, no usuarios arbitrarios.

Los métodos de defensa + inyección de interfaz podrían implementar completamente los métodos de extensión de estilo C #, pero AFAICS, la inyección de interfaz ni siquiera está en la hoja de ruta de Java 8 todavía.

Jörg W Mittag
fuente
3

Un poco tarde para la fiesta en esta pregunta, pero en caso de que alguien lo encuentre útil, acabo de crear una subclase:

public class ArrayList2<T> extends ArrayList<T> 
{
    private static final long serialVersionUID = 1L;

    public T getLast()
    {
        if (this.isEmpty())
        {
            return null;
        }
        else
        {       
            return this.get(this.size() - 1);
        }
    }
}
magritte
fuente
44
Los métodos de extensión son generalmente para código que no puede modificarse o heredarse como clases finales / selladas y su principal poder es la extensión de interfaces, por ejemplo, extender IEnumerable <T>. Por supuesto, son solo azúcar sintáctico para métodos estáticos. El propósito es que el código sea mucho más legible. Un código más limpio significa una mejor capacidad de mantenimiento / evolución.
mbx
1
Eso no es solo @mbx. Los métodos de extensión también son útiles para extender la funcionalidad de clase de clases no selladas, pero que no puede extender porque no controla lo que sea que devuelva instancias, por ejemplo, HttpContextBase, que es una clase abstracta.
Fabio Milheiro
@FabioMilheiro Incluí generosamente clases abstractas como "interfaces" en ese contexto. Las clases generadas automáticamente (xsd.exe) son del mismo tipo: podría pero no debería ampliarlas modificando los archivos generados. Normalmente los extendería utilizando "parcial", que requiere que residan en el mismo ensamblaje. Si no lo son, los métodos de extensión son una alternativa bastante atractiva. En última instancia, son solo métodos estáticos (no hay diferencia si observa el Código IL generado).
mbx
Sí ... HttpContextBase es una abstracción, aunque entiendo su generosidad. Llamar a una interfaz una abstracción podría haber parecido algo más natural. Independientemente de eso, no quise decir que tenía que ser una abstracción. Acabo de dar un ejemplo de una clase para la que escribí muchos métodos de extensión.
Fabio Milheiro
2

Podemos simular la implementación de los métodos de extensión de C # en Java utilizando la implementación del método predeterminado disponible desde Java 8. Comenzamos definiendo una interfaz que nos permitirá acceder al objeto de soporte a través de un método base (), de esta manera:

public interface Extension<T> {

    default T base() {
        return null;
    }
}

Devolvemos nulo ya que las interfaces no pueden tener estado, pero esto tiene que repararse más tarde a través de un proxy.

El desarrollador de extensiones tendría que extender esta interfaz mediante una nueva interfaz que contenga métodos de extensión. Supongamos que queremos agregar una interfaz para cada consumidor en la Lista:

public interface ListExtension<T> extends Extension<List<T>> {

    default void foreach(Consumer<T> consumer) {
        for (T item : base()) {
            consumer.accept(item);
        }
    }

}

Debido a que ampliamos la interfaz de Extensión, podemos llamar al método base () dentro de nuestro método de extensión para acceder al objeto de soporte al que nos adjuntamos.

La interfaz de extensión debe tener un método de fábrica que creará una extensión de un objeto de soporte dado:

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;
        }
    }
}

Creamos un proxy que implementa la interfaz de extensión y toda la interfaz implementada por el tipo del objeto de soporte. El controlador de invocación dado al proxy distribuiría todas las llamadas al objeto de soporte, excepto el método "base", que debe devolver el objeto de soporte, de lo contrario su implementación predeterminada devolverá nulo:

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);
    }

}

Luego, podemos usar el método Extension.create () para adjuntar la interfaz que contiene el método de extensión al objeto de soporte. El resultado es un objeto que puede ser lanzado a la interfaz de extensión por el cual todavía podemos acceder al objeto de soporte que llama al método base (). Al tener la referencia convertida en la interfaz de extensión, ahora podemos llamar con seguridad a los métodos de extensión que pueden tener acceso al objeto de soporte, de modo que ahora podemos adjuntar nuevos métodos al objeto existente, pero no a su tipo de definición:

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);
    }

}

Entonces, esta es una forma en que podemos simular la capacidad de extender objetos en Java al agregarles nuevos contratos, lo que nos permite llamar a métodos adicionales en los objetos dados.

A continuación puede encontrar el código de la interfaz de Extensión:

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;
        }
    }

}
Iulian Ilie-Némedi
fuente
1
eso es un gran infierno!
Dmitry Avtonomov
1

Se podría usar el patrón de diseño orientado a objetos decorador . Un ejemplo de este patrón que se usa en la biblioteca estándar de Java sería DataOutputStream .

Aquí hay un código para aumentar la funcionalidad de una Lista:

public class ListDecorator<E> implements List<E>
{
    public final List<E> wrapee;

    public ListDecorator(List<E> wrapee)
    {
        this.wrapee = wrapee;
    }

    // implementation of all the list's methods here...

    public <R> ListDecorator<R> map(Transform<E,R> transformer)
    {
        ArrayList<R> result = new ArrayList<R>(size());
        for (E element : this)
        {
            R transformed = transformer.transform(element);
            result.add(transformed);
        }
        return new ListDecorator<R>(result);
    }
}

PD: Soy un gran admirador de Kotlin . Tiene métodos de extensión y también se ejecuta en la JVM.

Eric
fuente
0

Puede crear un método auxiliar / extensión similar a C # mediante (RE) implementando la interfaz de Colecciones y agregando un ejemplo para la Colección Java:

public class RockCollection<T extends Comparable<T>> implements Collection<T> {
private Collection<T> _list = new ArrayList<T>();

//###########Custom extension methods###########

public T doSomething() {
    //do some stuff
    return _list  
}

//proper examples
public T find(Predicate<T> predicate) {
    return _list.stream()
            .filter(predicate)
            .findFirst()
            .get();
}

public List<T> findAll(Predicate<T> predicate) {
    return _list.stream()
            .filter(predicate)
            .collect(Collectors.<T>toList());
}

public String join(String joiner) {
    StringBuilder aggregate = new StringBuilder("");
    _list.forEach( item ->
        aggregate.append(item.toString() + joiner)
    );
    return aggregate.toString().substring(0, aggregate.length() - 1);
}

public List<T> reverse() {
    List<T> listToReverse = (List<T>)_list;
    Collections.reverse(listToReverse);
    return listToReverse;
}

public List<T> sort(Comparator<T> sortComparer) {
    List<T> listToReverse = (List<T>)_list;
    Collections.sort(listToReverse, sortComparer);
    return listToReverse;
}

public int sum() {
    List<T> list = (List<T>)_list;
    int total = 0;
    for (T aList : list) {
        total += Integer.parseInt(aList.toString());
    }
    return total;
}

public List<T> minus(RockCollection<T> listToMinus) {
    List<T> list = (List<T>)_list;
    int total = 0;
    listToMinus.forEach(list::remove);
    return list;
}

public Double average() {
    List<T> list = (List<T>)_list;
    Double total = 0.0;
    for (T aList : list) {
        total += Double.parseDouble(aList.toString());
    }
    return total / list.size();
}

public T first() {
    return _list.stream().findFirst().get();
            //.collect(Collectors.<T>toList());
}
public T last() {
    List<T> list = (List<T>)_list;
    return list.get(_list.size() - 1);
}
//##############################################
//Re-implement existing methods
@Override
public int size() {
    return _list.size();
}

@Override
public boolean isEmpty() {
    return _list == null || _list.size() == 0;
}
GarethReid
fuente
-7

Java8 ahora admite métodos predeterminados , que son similares a C#los métodos de extensión de.

DarVar
fuente
9
Incorrecto; El ejemplo en esta pregunta sigue siendo imposible.
SLaks
@SLaks, ¿cuál es la diferencia entre las extensiones Java y C #?
Fabio Milheiro
3
Los métodos predeterminados solo se pueden definir dentro de la interfaz. docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
SLaks
DarVar, este hilo de comentarios fue el único lugar donde se mencionaron los métodos predeterminados , que estaba tratando desesperadamente de recordar. ¡Gracias por mencionarlos, si no por su nombre! :-) (Gracias @SLaks por el enlace)
jpaugh
Votaría esa respuesta, porque el resultado final de un método estático en la interfaz proporcionaría el mismo uso que los métodos de extensión de C #, sin embargo, aún necesita implementar la interfaz en su clase a diferencia de C # que pasa (esta) palabra clave como parámetro
zaPlayer