在一个通用的Java方法中,替换非法的下限?

7

我希望能实现以下目标:

public class ImmutableList<T> {
  public <U super T> ImmutableList<U> add(U element) { ... }
}

也就是说,假设有一个不可变的 T 列表,您可以向列表中添加任何 U,从而产生一个不可变的 U 列表,但有一个约束条件,即 U 必须是 T 的超类型。例如:
  • 我可以将一只猴子添加到猴子列表中,得到一个新的猴子列表;
  • 我可以将一个人添加到猴子列表中,得到一个新的类人猿列表(假设猴子和人的最小上界是类人猿);
  • 我可以将一块石头添加到类人猿列表中,得到一个新的 Object 列表(假设石头和类人猿没有其他共同祖先)。
理论上听起来很好,但是根据 JLS 规定,U 的下界是不合法的。我可以改为写成:
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. JLS 第4.5.1节指出:“与在方法签名中声明的普通类型变量不同,在使用通配符时不需要进行类型推导。因此,在通配符上声明下限是允许的。”考虑到上面的另一种static方法签名,编译器显然能够推断出它需要的最小上限,所以这个争论似乎是错误的。有人可以反驳吗?

  3. 最重要的: 有人可以想到一个合法的替代我的带下限方法签名的方法吗?简而言之,目标是让调用代码看起来像Java (list.add(monkey))而不是Lisp (add(list, monkey))。


比你想象的还要糟糕:即使使用了你的第二个API设计,你也无法实现这个功能。使用类型擦除的话,你需要像传递类对象一样进行操作(例如 Class<U> clazz)。没有其他方法可以编写正确返回推断类型列表的代码。 - Ted Hopp
1
@TedHopp那是不正确的;擦除在这里并不相关。另一种Lisp式的设计非常容易实现,只是会产生丑陋的客户端代码。试试看。 - mergeconflict
1个回答

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

您至少可以在编译时强制执行类型之间的可转换性,作为额外的奖励,您可以定义自己的转换器,超越仅仅子类化。但是,如果没有转换器,您不能让Java找出应该使用适当的子类转换器作为默认值,这将使我们回到原始的API。


嗯,EmptyIL / NonEmptyIL 的实现方式几乎和我的一样(我称之为NilCons)。我的map实现方式也是一模一样的。你所添加的东西本质上是在add函数中使用了一个证明参数(类似于Scala中的“类型类模式”)。这是一个有趣的方法... 我现在不想马上选择它作为“正确”的答案——我想先尝试一下它——但它是个很好的选择 ;) - mergeconflict
我也认为这是证据。程序就像证明一样。 - Judge Mental
1
@JudgeMental:我很高兴地告诉你,自从Java 8和“目标类型”(http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#target-typing)的引入以来,Java类型推断变得稍微好了一点,`ImmutableList<Hominid> l = new EmptyIL<Chimpanzee>().add(monkey, id()).add(human, id());` 可以顺利编译。他们每次发布都越来越接近正确的Hindley-Milner推断。 :-) - Lii

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接