Заменить незаконные нижние границы на общий метод Java?


Я хотел бы сделать следующее:

public class ImmutableList<T> {
  public <U super T> ImmutableList<U> add(U element) { ... }
}
То есть, учитывая неизменяемый список T, Вы можете добавить любой U в список, чтобы получить неизменяемый список U, с ограничением, что U должен быть супертипом T. Например
  • я могу добавить обезьяну в список обезьян, получая новый список обезьян;
  • Я могу добавить человека к списку обезьян, получая новый список гоминидов (предположительно наименьшая верхняя граница обезьяны и человек); Я могу добавить камень к списку гоминидов, получая новый список Object (предполагая, что у камней и гоминидов нет других общих предков).
Это звучит здорово в теории, но нижняя граница U не является законной для JLS. Вместо этого я мог бы написать:
public class ImmutableList<T> {
  public ImmutableList<T> add(T element) { ... }
  public static <U> ImmutableList<U> add(ImmutableList<? extends U> list, U element) { ... }
}

Таким образом, компилятор правильно определит наименьшую верхнюю границу между типом элемента списка и U, который мы хотим добавить. Это законно. Это тоже отстой. Сравните:

// if 'U super T' were legal
list.add(monkey).add(human).add(rock);

// assuming 'import static ImmutableList.add'
add(add(add(list, monkey), human), rock);

Я-а большой поклонник функционального программирования, но я не хочу, чтобы мой Java-код выглядел как диалект Lisp. Итак, у меня есть три вопроса:

  1. ВТФ? Почему <U super T> связанное не является законным? Этот вопрос фактически был задан здесь раньше ("Почему параметр типа Java не может иметь нижнюю границу?") но я думаю, что вопрос, как он сформулирован там, несколько запутан, и ответы там сводятся к "это недостаточно полезно", чего я на самом деле не делаю. покупать.

  2. Раздел 4.5.1 JLS гласит: "В отличие от обычных переменных типа, объявленных в сигнатуре метода, вывод типа не требуется при использовании подстановочного знака. Следовательно, допустимо объявлять нижние границы на подстановочном знаке."Учитывая альтернативную сигнатуру метода static выше, компилятор явно способен вывести наименьшую верхнюю границу, в которой он нуждается, поэтому аргумент кажется сломанным. Может ли кто-нибудь возразить, что это не так?

  3. Самое главное: Может ли кто-нибудь придумать юридическую альтернативу моему методу подписи с нижней границей? Цель, в двух словах, состоит в том, чтобы иметь вызывающий код, который выглядит как Java (list.add(monkey)), а не Lisp (add(list, monkey)).

1 7

1 ответ:

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

public abstract class ImmutableList< T > {
    public interface Converter< U, V > {
        V convert( U u );
    }

    public abstract < U > ImmutableList< U > map( Converter< ? super T, ? extends U > cnv );

    public static class EmptyIL< T > extends ImmutableList< T >{
        @Override
        public < U > EmptyIL< U > map( Converter< ? super T, ? extends U > cnv ) {
            return new EmptyIL< U >();
        }
    }

    public static class NonEmptyIL< T > extends ImmutableList< T > {
        private final T tHead;
        private final ImmutableList< ? extends T > ltTail;
        public NonEmptyIL( T tHead, ImmutableList< ? extends T > ltTail ) {
            this.tHead = tHead;
            this.ltTail = ltTail;
        }
        @Override
        public < U > NonEmptyIL< U > map( Converter< ? super T, ? extends U > cnv ) {
            return new NonEmptyIL< U >( cnv.convert( tHead ), ltTail.map( cnv ) );
        }
    }

    public < U > ImmutableList< U > add( U u, final Converter< ? super T, ? extends U > cnv ) {
        return new NonEmptyIL< U >( u, map( cnv ) );
    }

    public static < V > Converter< V, V > id() {
        return new Converter< V, V >() {
            @Override public V convert( V u ) {
                return u;
            }
        };
    }

    public static < W, U extends W, V extends W > Converter< W, W > sup( U u, V v ) {
        return id();
    }

    static class Rock {}
    static class Hominid {}
    static class Human extends Hominid {}
    static class Monkey extends Hominid {}
    static class Chimpanzee extends Monkey {}

    public static void main( String[] args ) {
        Monkey monkey = new Monkey();
        Human human = new Human();
        Rock rock = new Rock();

        // id() should suffice, but doesn't
        new EmptyIL< Chimpanzee >().
            add( monkey, ImmutableList.< Monkey >id() ).
            add( human, ImmutableList.< Hominid >id() ).
            add( rock, ImmutableList.< Object >id() );

        // sup() finds the supremum of the two arguments' types and creates an identity conversion
        // but we have to remember what we last added
        new EmptyIL< Chimpanzee >().
            add( monkey, sup( monkey, monkey ) ).
            add( human, sup( monkey, human ) ). // add( human, sup( monkey, monkey ) ) also works
            add( rock, sup( human, rock ) );
    }
}

Вы, по крайней мере, получаете принудительное исполнение во время компиляции конвертируемости между типами, и в качестве дополнительного бонуса вы можете определить свои собственные конвертеры, которые выходят за рамки просто подклассов. Но вы не можете заставить Java понять, что в отсутствие конвертера он должен использовать соответствующий конвертер подклассов по умолчанию, что приведет нас к исходному API.