Обобщения Java - реализация функций высшего порядка, таких как map - PullRequest
8 голосов
/ 26 января 2011

Я решил написать некоторые общие функции высшего порядка в Java (отображение, фильтр, уменьшение и т. Д.), Которые являются типобезопасными с помощью обобщений, и у меня возникают проблемы с сопоставлением подстановочных знаков в одной конкретной функции.

Просто чтобы завершить, интерфейс функтора выглядит так:

/**
 * The interface containing the method used to map a sequence into another.
 * @param <S> The type of the elements in the source sequence.
 * @param <R> The type of the elements in the destination sequence.
 */
public interface Transformation<S, R> {

    /**
     * The method that will be used in map.
     * @param sourceObject An element from the source sequence.
     * @return The element in the destination sequence.
     */
    public R apply(S sourceObject);
}

Функция беспокойства похожа на карту , но вместо преобразования Collection она преобразует Карта (сначала я подумал, что ее следует назвать mapMap, но это звучало настолько глупо, что я в итоге назвал ее remapEntries).

Моя первая версия была (и взятьсидеть, потому что подпись довольно чудовищная):

    /**
     * <p>
     * Fills a map with the results of applying a mapping function to
     * a source map.
     * </p>
     * Considerations:
     * <ul>
     * <li>The result map must be non-null, and it's the same object what is returned
     * (to allow passing an unnamed new Map as argument).</li>
     * <li>If the result map already contained some elements, those won't
     * be cleared first.</li>
     * <li>If various elements have the same key, only the last entry given the
     * source iteration order will be present in the resulting map (it will
     * overwrite the previous ones).</li>
     * </ul>
     *
     * @param <SK> Type of the source keys.
     * @param <SV> Type of the source values.
     * @param <RK> Type of the result keys.
     * @param <RV> Type of the result values.
     * @param <MapRes>
     * @param f The object that will be used to remapEntries.
     * @param source The map with the source entries.
     * @param result The map where the resulting entries will be put.
     * @return the result map, containing the transformed entries.
     */
    public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) {
        for (Map.Entry<SK, SV> entry : source.entrySet()) {
            Map.Entry<RK, RV> res = f.apply(entry);
            result.put(res.getKey(), res.getValue());
        }
        return result;
    }

И это кажется вполне правильным, но проблема в том, что используемое преобразование должно точно соответствовать параметрам типа, что затрудняет повторное использование функций карты длятипы, которые совместимы.Поэтому я решил добавить в подпись символы подстановки, и все закончилось так:

public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) {
    for (Map.Entry<SK, SV> entry : source.entrySet()) {
        Map.Entry<? extends RK, ? extends RV> res = f.apply(entry);
        result.put(res.getKey(), res.getValue());
    }
    return result;
}

Но когда я пытаюсь это проверить, сопоставление с подстановочными знаками не удается:

@Test
public void testRemapEntries() {
    Map<String, Integer> things = new HashMap<String, Integer>();
    things.put("1", 1);
    things.put("2", 2);
    things.put("3", 3);

    Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() {
        public Entry<Integer, String> apply(Entry<String, Number> sourceObject) {
            return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry
        }
    };

    Map<Integer, String> expected = new HashMap<Integer, String>();
    expected.put(1, "1");
    expected.put(2, "2");
    expected.put(3, "3");

    Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>());
    assertEquals(expected, result);
}

ошибка:

method remapEntries in class IterUtil cannot be applied to given types
  required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes
  found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String>

Итак, есть ли подсказки, как это исправить?Или я должен отказаться и написать явные циклы для этого?^ _ ^

Ответы [ 3 ]

5 голосов
/ 26 января 2011

Это сложный вопрос.Следующие знания абсолютно бесполезны, и никто не должен заботиться о них:

Первое, что нужно исправить, это тип swap.Тип ввода не должен быть Entry<String,Number>, потому что тогда он не может принять Entry<String,Integer>, который не является подтипом E<S,N>.Однако E<S,I> является подтипом E<? extends S,? extends N>.Таким образом, наш трансформатор должен принять это как вход.Для вывода нет подстановочных знаков, потому что преобразователь может в любом случае только создавать конкретный тип.Мы просто хотим быть честными и точными в отношении того, что можно потреблять и что будет произведено:

    /*     */ Transformation<
                  Entry<? extends String, ? extends Number>, 
                  Entry<Integer, String>
              > swap
        = new Transformation<
                  Entry<? extends String, ? extends Number>, 
                  Entry<Integer, String>> () 
    {
        public Entry<Integer, String> apply(
            Entry<? extends String, ? extends Number> sourceObject) 
        {
            return new Pair<Integer, String>(
                sourceObject.getValue().intValue(), 
                sourceObject.getKey()
            );
        }
    };

Примечание String является окончательным и никто его не расширяет, но я боюсь, что общая система неэто умно знать, так что, в принципе, я все равно сделал ? extends String для дальнейшего блага.

Тогда, давайте подумаем о remapEntries().Мы подозреваем, что большинство передаваемых к нему преобразователей будет иметь объявление типа, аналогичное swap, из-за изложенных нами обоснований.Поэтому нам лучше иметь

remapEntry( 
    Transformation<
        Entry<? extends SK, ? extends SV>,
        Entry<RK,RV>
        > f,
    ...

, чтобы правильно сопоставить этот аргумент.Оттуда мы определяем тип источника и результата, мы хотим, чтобы они были как можно более общими:

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>>
RM remapEntries(
    Transformation<
        Entry<? extends SK, ? extends SV>,
        Entry<RK,RV>
        > f,
    Map<? extends SK, ? extends SV> source,
    RM result
)
{
    for(Entry<? extends SK, ? extends SV> entry : source.entrySet()) {
        Entry<RK,RV> res = f.apply(entry);
        result.put(res.getKey(), res.getValue());
    }
    return result;
}

RM не требуется, можно использовать напрямую Map<? super RK, ? super RV>.Но, похоже, вы хотите, чтобы возвращаемый тип был идентичным типу result в контексте вызывающего.Я бы просто сделал тип возврата void - проблем уже достаточно.

Эта вещь потерпит неудачу, если swap не использует ? extends.Например, если тип ввода String-Integer, смешно делать ? extends из них.Но у вас может быть метод перегрузки с другим объявлением типа параметра, чтобы соответствовать этому случаю.

Хорошо, это сработало, из чистой удачи.Но это совершенно не того стоит.Ваша жизнь станет намного лучше, если вы просто забудете об этом и будете использовать сырой тип, документировать параметры на английском языке, проверять тип во время выполнения.Спросите себя, универсальная версия покупает вам что-нибудь?Очень мало, по огромной цене, чтобы сделать ваш код совершенно непонятным.Никто, включая вас и меня, не сможет понять это, если мы прочитаем подпись метода завтра утром.Это намного хуже, чем регулярное выражение.

5 голосов
/ 26 января 2011

Думаю, вам стоит взглянуть на Google Guava API .

Там вы найдете интерфейс Function , аналогичный интерфейсу Transformation.Существует также класс Карты с служебными методами для создания или преобразования экземпляров карты.

При реализации методов для использования обобщений также следует учитывать PECS .

1 голос
/ 26 января 2011

Что-то внезапно появилось в моей голове: если подстановочные знаки во вложенных общих параметрах не будут захвачены, так как они являются буквально частью типа, то я мог бы использовать обратные границы на картах вместо использования их в Transformation.

public static <SK, SV, RK, RV, MapRes extends Map<? super RK, ? super RV>>
  MapRes remapEntries(final Transformation<Map.Entry<SK, SV>,
                                           Map.Entry<RK, RV>> f, 
                      final Map<? extends SK, ? extends SV> source,
                      MapRes result) {
    for (Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) {
        Map.Entry<? extends RK, ? extends RV> res = f.apply((Map.Entry<SK, SV>)entry);
        result.put(res.getKey(), res.getValue());
    }
    return result;
}

Единственная проблема заключается в том, что мы должны выполнить непроверенный бросок в Transformation.apply.Было бы совершенно безопасно, если бы интерфейс Map.Entry был только для чтения , поэтому мы можем просто скрестить пальцы и надеяться, что преобразование не попытается вызвать Map.Entry.setValue.

Мы могли быпо-прежнему передать неизменяемую оболочку интерфейса Map.Entry, которая вызвала исключение, если был вызван метод setValue для обеспечения как минимум безопасности типов во время выполнения.

Или просто создайте явный неизменный интерфейс Entry и используйте его, ноэто немного похоже на мошенничество (с двумя разными трансформациями):

public interface ImmutableEntry<K, V> {
    public K getKey();
    public V getValue();
}

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> RM remapEntries(final Transformation<ImmutableEntry<SK, SV>, Map.Entry<RK, RV>> f,
        final Map<? extends SK, ? extends SV> source,
        RM result) {
    for (final Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) {
        Map.Entry<? extends RK, ? extends RV> res = f.apply(new ImmutableEntry<SK, SV>() {
            public SK getKey() {return entry.getKey();}
            public SV getValue() {return entry.getValue();}
        });
        result.put(res.getKey(), res.getValue());
    }
    return result;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...