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

Substitute for illegal lower bounds on a generic Java method?

我想做以下工作:

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

也就是说,给定一个不可变的T列表,可以将任何U添加到该列表中,以生成一个不可变的U列表,其中的约束条件是U必须是T的父类型。例如

  • 我可以在猴子列表中添加一只猴子,生成一个新的猴子列表;
  • 我可以在猴子列表中添加一个人,生成一个新的原始人列表(大概是猴子和人类的最小上限);
  • 我可以在原始人列表中添加一块岩石,生成一个新的Object列表(假设岩石和原始人没有其他共同祖先)。
  • 小精灵

    这在理论上听起来很好,但是根据JLS,U的下限是不合法的。我可以写:

    1
    2
    3
    4
    public class ImmutableList<T> {
      public ImmutableList<T> add(T element) { ... }
      public static <U> ImmutableList<U> add(ImmutableList<? extends U> list, U element) { ... }
    }

    以这种方式,编译器将正确地推断列表的元素类型和我们要添加的U之间的最小上界。这是合法的。这也很糟糕。比较:

    1
    2
    3
    4
    5
    // 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方言。所以我有三个问题:

  • 世界跆拳道联盟?为什么绑定不合法?这个问题以前在这里被问过:"为什么Java类型的参数不能有一个下限?"但我认为问题的措辞有点混乱,答案归结为"它不够有用",我并不真正相信。

  • JLS第4.5.1节指出:"与方法签名中声明的普通类型变量不同,使用通配符时不需要进行类型推断。因此,允许在通配符上声明下界。"考虑到上面的可选static方法签名,编译器显然能够推断出它需要的最小上界,因此该参数似乎已被破坏。有人能说不是吗?

  • 最重要的是:有人能想出一个合法的替代方法来代替我的下界方法签名吗?简而言之,目标是调用看起来像Java(EDCOX1(10))的代码,而不是LISP(EDCOX1(11))。


  • 你几乎可以做到这一点,但Java类型的推理设法吮吸只是足以使它不值得。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    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。