Java еквівалентна методам розширення C #


176

Я шукаю реалізувати функціональність у списку об’єктів, як я б в C #, використовуючи метод розширення.

Щось на зразок цього:

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

Як це зробити на Java?


8
Перевірте це: github.com/nicholas22/jpropel, example: new String [] {"james", "john", "john", "eddie"} .wherewhere (beginWith ("j")). Razloct (); Він використовує lombok-pg, що забезпечує хороший спосіб розширення.
NT_

6
Microsoft, безумовно, правильно зрозуміла, коли дозволила розширення. Підкласифікація для додавання нової функціональності не працює, якщо мені потрібна функція в класі, повернутому мені в іншому місці. Як і додавання методів до рядка та дати.
tggagne

3
тобто java.lang.String - це заключний клас, тому ви не можете продовжити його. Використання статичних методів - це спосіб, але він показує, що код іноді не читається. Я думаю, що C # залишив вік як комп'ютерний язик. Методи розширення, часткові класи, LINQ і так далі ..
Davut Gürbüz

7
@Roadrunner, rofl! Найкращим прихильником відсутньої мови є те, що зазначена відсутність мови є злою та небажаною. Це відомо.
Кірк Волл

6
Методи розширення не є «злими». Вони значно покращують читабельність коду. Ще одне з багатьох поганих дизайнерських рішень на Java.
csauve

Відповіді:


196

Java не підтримує методи розширення.

Натомість ви можете зробити звичайний статичний метод або написати власний клас.


63
Я зіпсований після використання методів розширення - але статичні методи теж зроблять трюк.
bbqchickenrobot

31
Але синтаксис такий приємний і полегшує розуміння програми :) Мені також подобається, як Ruby дозволяє зробити майже те саме, за винятком того, що ви можете фактично змінювати вбудовані в класи та додавати нові методи.
knownasilya

18
@Ken: Так, і в цьому вся суть! Чому ви пишете на Java, а не безпосередньо в байт-код JVM? Чи не "це лише питання синтаксису"?
Федір Сойкін

30
Методи розширення можуть зробити код набагато елегантнішим порівняно з додатковими статичними методами з іншого класу. Практично всі більш сучасні мови дозволяють мати якесь розширення існуючого класу: C #, php, object-c, javascript. Ява тут неодмінно показує свій вік. Уявіть, що ви хочете записати JSONObject на диск. Ви називаєте jsonobj.writeToDisk () або який-небудь unrelatedclass.writeToDisk (jsonobj)?
woens

8
Причини ненавидіти Яву постійно зростають. І я перестав їх шукати пару років тому ......
Джон Деметріу

54

Методи розширення - це не просто статичний метод і не просто зручність синтаксичного цукру, адже вони є досить потужним інструментом. Головне - це можливість переосмислювати різні методи, що базуються на інстанціонуванні різних загальних параметрів. Це схоже на типи класів Haskell, і насправді, схоже, вони перебувають у C # для підтримки C # 's Monads (тобто LINQ). Навіть скидаючи синтаксис LINQ, я все ще не знаю жодного способу реалізації подібних інтерфейсів у Java.

І я не думаю, що це можливо реалізувати в Java через семантику стирання стилю Java для генеричних параметрів.


Вони також дозволяють успадкувати множинні поведінки (sans polymorphism). Ви можете реалізовувати декілька інтерфейсів, а разом із цим приходять методи їх розширення. Вони також дозволяють реалізувати поведінку, яку ви хочете приєднати до типу, не маючи його глобально асоційованого з типом для всієї системи.
Розумний неологізм

25
Вся ця відповідь неправильна. Методи розширення в C # - це лише синтаксичний цукор, який компілятор трохи переставляє для переміщення цілі виклику методу до першого аргументу статичного методу. Не можна перекривати існуючі методи. Немає вимоги, щоб метод розширення був монадою. Це буквально просто більш зручний спосіб викликати статичний метод, який надає зовнішній вигляд додавання методів екземплярів до класу. Прочитайте, якщо ви згодні з цією відповіддю
Метт Кляйн

3
добре, у цьому випадку визначте, що таке синтаксичний цукор, я б назвав синтаксичний цукор внутрішнім макросинтаксисом, тому що компілятор методів розширення повинен принаймні шукати статичний клас, який розширюється методом розширення. Немає нічого у відповіді про те, що метод повинен бути монадою, що не має сенсу. Крім того, ви можете використовувати його для перевантаження, але це не особливості методів розширення, це звичайний тип параметрів на основі перевантаження, так само, як він би працював, якщо метод викликається безпосередньо, і він не працюватиме у багатьох цікавих випадках на Java через стирання аргументів типу generics.
користувач1686250

1
@ user1686250 Можна реалізувати в Java (під "Java", мабуть, ви маєте на увазі байт-код, який працює на JVM) ... Kotlin, який компілює в байт-код, має розширення. Це просто синтаксичний цукор над статичними методами. Ви можете використовувати декомпілятор в IntelliJ, щоб побачити, як виглядає еквівалентна Java.
Джефрі Блатман

@ user1686250 Чи можете ви розробити те, що ви пишете про дженерики (або надати посилання), оскільки я абсолютно не розумію, що стосується генерики. Яким чином, крім звичайних статичних методів, це пов'язано?
Ч. Шампань

17

Project Lombok надає примітку, @ExtensionMethodяка може бути використана для досягнення потрібної вам функції.


3
Колектор забезпечує комплексну, безшовні C # -стилі метод розширення підтримки Java , що дозволяє додавати методи класів , які ви не контролюєте , наприклад, java.lang.String. Демонстрація: http://manifold.systems/images/ExtensionMethod.mp4
Скотт

10

Технічно розширення C # не мають еквівалента на Java. Але якщо ви хочете реалізувати такі функції для отримання більш чистого коду та ремонту, вам доведеться використовувати рамку 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();
  }
}

7

Мова XTend - це супернабір Java та компілюється у вихідний код Java 1  - це підтримує.


Коли той код, який не є Java, компілюється в Java, чи є у вас метод розширення? Або код Java - лише статичний метод?
Фабіо Мілхейро

@Bomboca Як зазначали інші, у Java немає методів розширення. Отже, код XTend, скомпільований на Java, якимось чином не створює метод розширення Java. Але якщо ви працюєте виключно в XTend, ви не помітите і не потурбуєтесь. Але, щоб відповісти на ваше запитання, статичний метод також не обов'язково є. Головний автор XTend має запис про це в блозі
Erick G. Hagstrom

Так, не знаю, чому я теж не думав про це. Дякую!
Фабіо Мілхейро

@Sam Дякую, що познайомили мене з XTend - я ніколи про це не чув.
jpaugh

7

Manifold надає Java способи розширення C # -style та кілька інших функцій. В відміну від інших інструментів, Колектор не має ніяких обмежень і не страждає від проблем з дженерик, лямбда, IDE і т.д. Колектор надає ряд інших функцій , такі як F # -Style призначених для користувача типів , машинопис стилю структурних інтерфейсів і Javascript-стиль типів EXPANDO .

Крім того, IntelliJ забезпечує всебічну підтримку Manifold через плагін Manifold .

Колектор - це проект з відкритим кодом, доступний на github .



5

У Java такої функції немає. Натомість ви можете або створити звичайний підклас реалізації списку, або створити анонімний внутрішній клас:

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

Проблема полягає в тому, щоб викликати цей метод. Ви можете це зробити "на місці":

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

112
Це абсолютно марно.
СЛАкс

2
@Slaks: Чому саме? Це "написати свій клас", запропонований вами.
Горан Йович

23
@Goran: Все це дозволяє вам визначити метод, а потім викликати його негайно, раз .
СЛАкс

3
@Slaks: Гаразд, точка. У порівнянні з цим обмеженим рішенням, краще писати названий клас.
Горан Йович

Існує велика, велика різниця між методами розширення C # та анонімними класами Java. У C # метод розширення - це синтаксичний цукор для того, що насправді є лише статичним методом. IDE і компілятор роблять метод розширення схожим на те, що це метод екземпляра розширеного класу. (Примітка: "розширений" у цьому контексті не означає "успадкований", як це було б зазвичай на Яві.)
HairOfTheDog

4

Схоже , є деякі невеликі шанси , що Захисник методу (тобто методу по замовчуванням) може зробити це в Java 8. Тим НЕ менше, наскільки я їх розумію, вони тільки дозволяють автор про interfaceзадньому числі продовжити його, а не довільно користувач.

Методи Defender + Ін'єкція інтерфейсу зможе повністю реалізувати методи розширення C # -style, але AFAICS, Інжекція інтерфейсу ще не існує на дорожній карті Java 8.


3

Трохи запізнюємось із учасником цього питання, але якщо хтось вважає це корисним, я просто створив підклас:

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

4
Методи розширення, як правило, для коду, який не може бути змінений або успадкований, як остаточні / запечатані класи, і його основною потужністю є розширення інтерфейсів, наприклад, розширення IEnumerable <T>. Звичайно, вони є лише синтаксичним цукром для статичних методів. Мета полягає в тому, щоб код був набагато читабельнішим. Чистіший код означає кращу експлуатаційну здатність / еволюційність.
mbx

1
Це не тільки це @mbx. Методи розширення також корисні для розширення функціональності класів незапечатаних класів, але які ви не можете розширити, оскільки ви не контролюєте те, що повертає екземпляри, наприклад HttpContextBase, який є абстрактним класом.
Фабіо Мілхейро

@FabioMilheiro Я щедро включив абстрактні класи як "інтерфейси" в цьому контексті. Автоматично створені класи (xsd.exe) мають один і той же вид: ви можете, але не слід поширювати їх, змінюючи створені файли. Зазвичай ви подовжуєте їх, використовуючи "частковий", який вимагає їх перебування в одній збірці. Якщо їх немає, способи розширення є досить цікавою альтернативою. Зрештою, вони є лише статичними методами (різниці немає, якщо подивитися на створений ІЛ-код).
mbx

Так ... HttpContextBase - це абстракція, хоча я розумію вашу щедрість. Називання інтерфейсу абстракцією могло здатися дещо природнішим. Незалежно від цього, я не мав на увазі, що це має бути абстракція. Я просто наводив приклад класу, для якого я написав багато методів розширення.
Фабіо Мілхейро

2

Ми можемо імітувати реалізацію методів розширення C # у Java, використовуючи реалізацію методу за замовчуванням, доступну з Java 8. Почнемо з визначення інтерфейсу, який дозволить нам отримати доступ до об’єкта підтримки за допомогою методу base (), наприклад:

public interface Extension<T> {

    default T base() {
        return null;
    }
}

Ми повертаємо null, оскільки інтерфейси не можуть мати стан, але це потрібно виправити пізніше через проксі.

Розробник розширень повинен був би розширити цей інтерфейс новим інтерфейсом, що містить методи розширення. Скажімо, ми хочемо додати ForEach споживача до інтерфейсу List:

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

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

}

Оскільки ми розширюємо інтерфейс розширення, ми можемо викликати метод base () всередині нашого методу розширення для доступу до об'єкта підтримки, до якого ми приєднуємося.

Інтерфейс розширення повинен мати заводський метод, який створить розширення заданого об’єкта підтримки:

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

Ми створюємо проксі-сервер, який реалізує інтерфейс розширення та весь інтерфейс, реалізований за типом об'єкта підтримки. Обробник виклику, наданий проксі, відправив би всі виклики до об'єкта підтримки, за винятком методу "base", який повинен повернути об'єкт підтримки, інакше його реалізація за замовчуванням повертає нуль:

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

}

Тоді ми можемо використовувати метод Extension.create () для приєднання інтерфейсу, що містить метод розширення, до об'єкта підтримки. Результатом є об'єкт, який можна подати до інтерфейсу розширення, за допомогою якого ми все ще можемо отримати доступ до об'єкта підтримки, що викликає метод base (). Маючи посилання на інтерфейс розширення, ми тепер можемо сміливо викликати методи розширення, які можуть мати доступ до об'єкта підтримки, так що тепер ми можемо приєднати нові методи до існуючого об'єкта, але не до його визначеного типу:

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

}

Отже, це спосіб ми змоделюємо можливість розширення об’єктів на Java, додаючи до них нові контракти, що дозволяють викликати додаткові методи на заданих об’єктах.

Нижче ви знайдете код інтерфейсу розширення:

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

}

1
це одне пекло хитрості!
Дмитро Автономов

1

Можна використати об'єктно-орієнтований шаблон дизайну декоратора . Прикладом цієї схеми, що використовується в стандартній бібліотеці Java, буде DataOutputStream .

Ось код для розширення функціональності списку:

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 Я великий фанат Котліна . Він має методи розширення, а також працює на JVM.


0

Ви можете створити розширення / допоміжний метод типу C #, використовуючи (RE) реалізацію інтерфейсу Collections та додаючи приклад для 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;
}

-7

Java8 тепер підтримує методи за замовчуванням , подібні до C#методів розширення.


9
Неправильно; приклад у цьому питанні все ще неможливий.
СЛАкс

@SLaks Яка різниця між розширеннями Java та C #?
Фабіо Мілхейро

3
Методи за замовчуванням можна визначити лише в інтерфейсі. docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
SLaks

DarVar, Цей потік коментарів був єдиним місцем, де згадувалися методи за замовчуванням , які я відчайдушно намагався запам'ятати. Дякуємо, що згадали про них, якщо не по імені! :-) (Спасибі @SLaks за посилання)
jpaugh

Я б підтримав цю відповідь, оскільки кінцевий результат статичного методу в інтерфейсі забезпечив би те саме використання, що і методи розширення C #, однак, вам все одно потрібно реалізувати інтерфейс у своєму класі на відміну від C # ви передаєте (це) ключове слово як параметр
zaPlayer
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.