替换泛型Java方法的非法下限?

mer*_*ict 7 java generics type-inference lower-bound

我想做以下事情:

public class ImmutableList<T> {
  public <U super T> ImmutableList<U> add(U element) { ... }
}
Run Code Online (Sandbox Code Playgroud)

也就是说,给定一个不可变列表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) { ... }
}
Run Code Online (Sandbox Code Playgroud)

以这种方式,编译器将正确地推断列表的元素类型和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);
Run Code Online (Sandbox Code Playgroud)

我是函数式编程的忠实粉丝,但我不希望我的Java代码看起来像Lisp方言.所以我有三个问题:

  1. WTF?为什么<U super T>绑定不合法?这个问题之前已经在这里被问到了("为什么Java类型参数不能有下限?")但我认为那里的问题有点混乱,而那里的答案归结为"它没有用,"我真的不买.

  2. JLS第4.5.1节规定:"与方法签名中声明的普通类型变量不同,使用通配符时不需要类型推断.因此,允许在通配符上声明下限." 鉴于static上面的替代方法签名,编译器显然能够推断出它需要的最小上限,因此参数似乎被打破了.有人可以争辩说不是吗?

  3. 最重要的是:任何人都可以考虑使用下限绑定我的方法签名吗?简而言之,目标是让代码看起来像Java(list.add(monkey))而不是Lisp(add(list, monkey)).

Jud*_*tal 2

您几乎可以做到这一点,但是 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 ) );
    }
}
Run Code Online (Sandbox Code Playgroud)

您至少可以在编译时强制执行类型之间的可转换性,并且作为额外的好处,您可以定义自己的转换器,而不仅仅是子类化。但是你不能让 Java 知道在没有转换器的情况下它应该使用适当的子类化转换器作为默认值,这会将我们带到你的原始 API。