Java odpowiednik metod rozszerzających C #

176

Szukam zaimplementowania funkcjonalności na liście obiektów, tak jak w C # przy użyciu metody rozszerzenia.

Coś takiego:

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

Jak to zrobić w Javie?

Fabio Milheiro
źródło
8
Sprawdź to: github.com/nicholas22/jpropel, przykład: nowy ciąg [] {"james", "jan", "jan", "eddie"}. Gdzie (zaczyna się z ("j")). Wyraźny (); Używa lombok-pg, który zapewnia dobroć metody rozszerzenia.
NT_
6
Microsoft zdecydowanie dobrze to zrobił, zezwalając na rozszerzenia. Tworzenie podklas w celu dodania nowej funkcjonalności nie działa, jeśli potrzebuję funkcji w klasie zwróconej w innym miejscu. Podobnie jak dodawanie metod do String i Date.
tggagne
3
tj. java.lang.String jest ostatnią klasą, więc nie można jej rozszerzyć. Używanie metod statycznych jest sposobem, ale czasami pokazuje kod nieczytelny. Myślę, że C # opuścił wiek jako język komputera. Metody rozszerzające, klasy częściowe, LINQ i tak dalej ..
Davut Gürbüz
7
@Roadrunner, rofl! Najlepszą odpowiedzią na brak funkcji językowej jest to, że wspomniana brakująca funkcja językowa jest zła i niepożądana. To jest znane.
Kirk Woll,
6
Metody rozszerzające nie są „złe”. Znacznie poprawiają czytelność kodu. Jeszcze jedna z wielu kiepskich decyzji projektowych w Javie.
csauve

Odpowiedzi:

196

Java nie obsługuje metod rozszerzających.

Zamiast tego możesz utworzyć zwykłą metodę statyczną lub napisać własną klasę.

SLaks
źródło
63
Jestem zepsuty po użyciu metod rozszerzających - ale metody statyczne również wystarczą.
bbqchickenrobot
31
Ale składnia jest bardzo ładna i sprawia, że ​​program jest łatwiejszy do zrozumienia :) Podoba mi się również to, jak Ruby pozwala ci robić prawie to samo, z wyjątkiem tego, że możesz faktycznie modyfikować wbudowane klasy i dodawać nowe metody.
knownasilya
18
@Ken: Tak, o to chodzi! Dlaczego piszesz w Javie, a nie bezpośrednio w kodzie bajtowym JVM? Czy nie jest to „tylko kwestia składni”?
Fyodor Soikin
30
Metody rozszerzające mogą uczynić kod bardziej eleganckim w porównaniu z dodatkowymi metodami statycznymi z innej klasy. Prawie wszystkie nowsze języki pozwalają na jakieś istniejące rozszerzenie klasy: C #, php, objective-c, javascript. Java z pewnością pokazuje tutaj swój wiek. Wyobraź sobie, że chcesz zapisać obiekt JSONObject na dysku. Czy wywołujesz jsonobj.writeToDisk () lub jakiśunrelatedclass.writeToDisk (jsonobj)?
woens
8
Powody, dla których warto nienawidzić Javy, wciąż rosną. I przestałem ich szukać kilka lat temu ......
John Demetriou
54

Metody rozszerzeń to nie tylko metoda statyczna, a nie tylko wygodna składnia, w rzeczywistości są one dość potężnym narzędziem. Najważniejsza jest możliwość nadpisywania różnych metod opartych na instancji różnych parametrów generycznych. Jest to podobne do klas typu Haskella i faktycznie wygląda na to, że są w C #, aby obsługiwać monady C # (tj. LINQ). Nawet porzucając składnię LINQ, nadal nie wiem, jak zaimplementować podobne interfejsy w Javie.

I nie sądzę, aby można je było zaimplementować w Javie, ze względu na semantykę parametrów generycznych w Javie.

user1686250
źródło
Pozwalają również na dziedziczenie wielu zachowań (bez polimorfizmu). Możesz zaimplementować wiele interfejsów, a wraz z tym pojawiają się ich metody rozszerzające. Pozwalają również na implementację zachowania, które chcesz dołączyć do typu, bez globalnego powiązania go z typem w całym systemie.
Clever Neologism
25
Cała odpowiedź jest błędna. Metody rozszerzające w języku C # to po prostu cukier składniowy, który kompilator nieco przestawia, aby przenieść cel wywołania metody do pierwszego argumentu metody statycznej. Nie możesz zastąpić istniejących metod. Nie ma wymogu, aby metoda rozszerzenia była monadą. Jest to po prostu wygodniejszy sposób wywołania metody statycznej, który sprawia wrażenie dodawania metod instancji do klasy. Przeczytaj to, jeśli zgadzasz się z tą odpowiedzią
Matt Klein,
3
cóż, w tym przypadku zdefiniuj, czym jest cukier składniowy, nazwałbym cukier składni jako wewnętrzną składnię makra, ponieważ kompilator metod rozszerzających musi przynajmniej sprawdzić klasę statyczną, którą ma zastąpić metoda rozszerzenia. W odpowiedzi nie ma nic na temat metody, która powinna być monadą, co jest bezsensowne. Można go również użyć do przeciążania, ale nie jest to funkcja metod rozszerzających, jest to przeciążanie oparte na typie zwykłego parametru, tak samo działałoby, gdyby metoda była wywoływana bezpośrednio, i nie będzie działać w wielu interesujących przypadkach w Javie z powodu wymazywania argumentów typu ogólnego.
user1686250
1
@ user1686250 Można to zaimplementować w Javie (przez "Javę" rozumiem kod bajtowy działający na JVM) ... Kotlin, który kompiluje się do kodu bajtowego ma rozszerzenia. To tylko cukier syntaktyczny w porównaniu z metodami statycznymi. Możesz użyć dekompilatora w IntelliJ, aby zobaczyć, jak wygląda odpowiednik Java.
Jeffrey Blattman,
@ user1686250 Czy mógłbyś rozwinąć to, co piszesz o rodzajach generycznych (lub podać link), ponieważ absolutnie nie rozumiem sensu o rodzajach. W jaki sposób inne niż zwykłe metody statyczne są powiązane?
C.Champagne
10

Technicznie rzecz biorąc, rozszerzenia C # nie mają odpowiednika w Javie. Ale jeśli chcesz zaimplementować takie funkcje, aby uzyskać bardziej przejrzysty kod i łatwość konserwacji, musisz użyć struktury 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
źródło
7

Język XTend - który jest super zestawem języków Java i kompiluje się do kodu źródłowego Java 1  - obsługuje to.

Sam Keays
źródło
Kiedy kod, który nie jest językiem Java, jest kompilowany do języka Java, czy istnieje metoda rozszerzenia? Czy kod Java jest tylko metodą statyczną?
Fabio Milheiro
@Bomboca Jak zauważyli inni, Java nie ma metod rozszerzających. Tak więc kod XTend, skompilowany do Javy, w jakiś sposób nie tworzy metody rozszerzenia Java. Ale jeśli pracujesz wyłącznie w XTend, nie zauważysz ani nie będziesz się tym przejmować. Ale odpowiadając na twoje pytanie, niekoniecznie masz też metodę statyczną. Główny autor XTend ma wpis na blogu na ten temat pod adresem blog.efftinge.de/2011/11/…
Erick G. Hagstrom
Tak, nie wiem, dlaczego ja też tak nie pomyślałem. Dzięki!
Fabio Milheiro
@Sam Dzięki za wprowadzenie mnie do XTend - nigdy o tym nie słyszałem.
jpaugh
7

Manifold udostępnia Javę z metodami rozszerzeń w stylu C # i kilkoma innymi funkcjami. W przeciwieństwie do innych narzędzi, Manifold nie ma ograniczeń i nie cierpi z powodu problemów z typami ogólnymi, lambdami, IDE itp. Manifold zapewnia kilka innych funkcji, takich jak niestandardowe typy w stylu F # , strukturalne interfejsy w stylu TypeScript i typy expando w stylu JavaScript .

Dodatkowo IntelliJ zapewnia kompleksową obsługę Manifolda poprzez wtyczkę Manifold .

Manifold to projekt open source dostępny na github .

Scott
źródło
5

Java nie ma takiej funkcji. Zamiast tego możesz utworzyć zwykłą podklasę swojej implementacji listy lub utworzyć anonimową klasę wewnętrzną:

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

Problem polega na wywołaniu tej metody. Możesz to zrobić „na miejscu”:

new ArrayList<String>() {
   public String getData() {
       return ""; // add your implementation here. 
   }
}.getData();
AlexR
źródło
112
To jest całkowicie bezużyteczne.
SLaks
2
@Slaks: Dlaczego dokładnie? To jest „napisz własną klasę”, którą zasugerowałeś.
Goran Jovic
23
@Goran: Wszystko to pozwala zrobić, to określić metodę, a następnie wywołać ją natychmiast, raz .
Slaks
3
@Slaks: W porządku, uwaga. W porównaniu z tym ograniczonym rozwiązaniem, napisanie nazwanej klasy byłoby lepsze.
Goran Jovic
Istnieje duża, duża różnica między metodami rozszerzającymi C # a anonimowymi klasami Java. W języku C # metoda rozszerzenia jest cukrem składniowym dla tego, co w rzeczywistości jest metodą statyczną. IDE i kompilator sprawiają, że metoda rozszerzająca wygląda tak, jakby była metodą instancji klasy rozszerzonej. (Uwaga: „rozszerzone” w tym kontekście nie oznacza „dziedziczone”, jak to zwykle bywa w Javie.)
HairOfTheDog
4

Wygląda na to jest jakaś mała szansa, że Defender (tj Metody Metody domyślne) może uczynić go Java 8. Jednakże o ile mi ich zrozumieć, pozwalają one tylko autor od Aninterface wstecznie przedłużyć go, a nie arbitralnych użytkowników.

Defender Methods + Interface Injection byłby wtedy w stanie w pełni zaimplementować metody rozszerzeń w stylu C #, ale AFAICS, Interface Injection nie jest jeszcze na mapie drogowej Java 8.

Jörg W Mittag
źródło
3

Trochę za późno na imprezę w tej sprawie, ale na wypadek, gdyby ktoś uznał to za przydatne, właśnie utworzyłem podklasę:

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
źródło
4
Metody rozszerzające są zwykle przeznaczone dla kodu, którego nie można modyfikować ani dziedziczyć, tak jak klasy końcowe / zapieczętowane, a ich główną mocą jest rozszerzenie interfejsów, np. Rozszerzenie IEnumerable <T>. Oczywiście są one tylko cukrem syntaktycznym dla metod statycznych. Celem jest, aby kod był znacznie bardziej czytelny. Czystszy kod oznacza lepszą konserwowalność / możliwość ewolucji.
mbx
1
To nie tylko to @mbx. Metody rozszerzające są również przydatne do rozszerzania funkcjonalności klas niezamkniętych, ale których nie można rozszerzać, ponieważ nie kontroluje się tego, co zwraca zwracane wystąpienia, np. HttpContextBase, która jest klasą abstrakcyjną.
Fabio Milheiro
@FabioMilheiro W tym kontekście hojnie zawarłem klasy abstrakcyjne jako „interfejsy”. Klasy generowane automatycznie (xsd.exe) są tego samego rodzaju: można, ale nie należy ich rozszerzać, modyfikując wygenerowane pliki. Zwykle można je rozszerzyć za pomocą „częściowego”, co wymaga, aby rezydowały w tym samym zestawie. Jeśli tak nie jest, metody rozszerzające są ładną alternatywą. Ostatecznie są to tylko metody statyczne (nie ma różnicy, jeśli spojrzymy na wygenerowany kod IL).
mbx
Tak ... HttpContextBase to abstrakcja, chociaż rozumiem twoją hojność. Nazywanie interfejsu abstrakcją mogłoby wydawać się nieco bardziej naturalne. Niezależnie od tego nie miałem na myśli, że to musi być abstrakcja. Właśnie podałem przykład klasy, dla której napisałem wiele metod rozszerzających.
Fabio Milheiro,
2

Możemy zasymulować implementację metod rozszerzających C # w Javie, używając domyślnej implementacji metody dostępnej od Java 8. Zaczynamy od zdefiniowania interfejsu, który pozwoli nam na dostęp do obiektu wsparcia za pomocą metody base (), na przykład:

public interface Extension<T> {

    default T base() {
        return null;
    }
}

Zwracamy wartość null, ponieważ interfejsy nie mogą mieć stanu, ale należy to później naprawić za pośrednictwem serwera proxy.

Twórca rozszerzeń musiałby rozszerzyć ten interfejs o nowy interfejs zawierający metody rozszerzające. Powiedzmy, że chcemy dodać konsumenta forEach do interfejsu List:

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

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

}

Ponieważ rozszerzamy interfejs Extension, możemy wywołać metodę base () wewnątrz naszej metody rozszerzenia, aby uzyskać dostęp do obiektu wsparcia, do którego się podłączamy.

Interfejs Extension musi posiadać metodę fabryczną, która utworzy rozszerzenie danego obiektu wsparcia:

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

Tworzymy proxy, które implementuje interfejs rozszerzenia i cały interfejs zaimplementowany przez typ obiektu wsparcia. Procedura obsługi wywołań przekazana proxy mogłaby wysłać wszystkie wywołania do obiektu wsparcia, z wyjątkiem metody „podstawowej”, która musi zwrócić obiekt wsparcia, w przeciwnym razie jej domyślna implementacja zwraca wartość 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);
    }

}

Następnie możemy użyć metody Extension.create (), aby dołączyć interfejs zawierający metodę rozszerzenia do obiektu pomocniczego. Rezultatem jest obiekt, który można rzutować na interfejs rozszerzenia, za pomocą którego nadal możemy uzyskać dostęp do obiektu pomocniczego wywołującego metodę base (). Mając referencję rzuconą na interfejs rozszerzenia, możemy teraz bezpiecznie wywołać metody rozszerzające, które mogą mieć dostęp do obiektu wsparcia, dzięki czemu możemy teraz dołączyć nowe metody do istniejącego obiektu, ale nie do jego definiującego typu:

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

}

Jest to więc sposób, w jaki możemy zasymulować możliwość rozszerzania obiektów w Javie poprzez dodawanie do nich nowych kontraktów, które pozwalają nam wywoływać dodatkowe metody na danych obiektach.

Poniżej znajduje się kod interfejsu rozszerzenia:

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
źródło
1
to jest piekielna bzdura!
Dmitry Avtonomov
1

Można by wykorzystać wzorzec projektowania obiektowego dekoratora . Przykładem tego wzorca używanego w standardowej bibliotece Javy byłby DataOutputStream .

Oto kod rozszerzający funkcjonalność listy:

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

PS Jestem wielkim fanem Kotlina . Ma metody rozszerzające, a także działa na JVM.

Eric
źródło
0

Możesz stworzyć metodę rozszerzenia / pomocnika podobną do C #, implementując (RE) interfejs Collections i dodając - przykład dla Java Collection:

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
źródło
-7

Java8 obsługuje teraz metody domyślne , które są podobne do C#metod rozszerzających.

DarVar
źródło
9
Źle; przykład w tym pytaniu jest nadal niemożliwy.
SLaks
@SLaks Jaka jest różnica między rozszerzeniami Java i C #?
Fabio Milheiro,
3
Metody domyślne można zdefiniować tylko w interfejsie. docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
SLaks
DarVar, Ten wątek komentarza był jedynym miejscem, w którym wspomniano o domyślnych metodach , które desperacko próbowałem zapamiętać. Dziękuję za wspomnienie o nich, jeśli nie po imieniu! :-) (Dzięki @SLaks za link)
jpaugh
Głosowałbym za tą odpowiedzią, ponieważ końcowy wynik ze statycznej metody w interfejsie zapewniłby to samo zastosowanie, co metody rozszerzające C #, jednak nadal musisz zaimplementować interfejs w swojej klasie, w przeciwieństwie do C #, który przekazujesz (this) słowo kluczowe jako parametr
zaPlayer,