【问题标题】:Casting Java functional interfaces铸造 Java 函数式接口
【发布时间】:2017-05-31 15:27:19
【问题描述】:

和往常一样,我浏览了 JDK 8 的源代码,发现了非常有趣的代码:

@Override
default void forEachRemaining(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
        forEachRemaining((IntConsumer) action);
    } 
}

问题是:Consumer&lt;? super Integer&gt; 怎么可能是IntConsumer实例?因为它们处于不同的层次结构中。

我已经做了类似的代码 sn-p 来测试铸造:

public class InterfaceExample {
    public static void main(String[] args) {
        IntConsumer intConsumer = i -> { };
        Consumer<Integer> a = (Consumer<Integer>) intConsumer;

        a.accept(123);
    }
}

但它会抛出 ClassCastException:

Exception in thread "main" 
    java.lang.ClassCastException: 
       com.example.InterfaceExample$$Lambda$1/764977973 
     cannot be cast to 
       java.util.function.Consumer

您可以在java.util.Spliterator.OfInt#forEachRemaining(java.util.function.Consumer)找到此代码

【问题讨论】:

    标签: java lambda java-8 classcastexception functional-interface


    【解决方案1】:

    让我们看看下面的代码,你就知道为什么了吗?

    class IntegerConsumer implements Consumer<Integer>, IntConsumer {
       ...
    }
    

    任何类都可以实现多接口,一个是Consumer&lt;Integer&gt; 可能实现另一个是IntConsumer。有时当我们想将IntConsumer适配为Consumer&lt;Integer&gt;并保存其原始类型(IntConsumer)时,代码如下所示:

    class IntConsumerAdapter implements Consumer<Integer>, IntConsumer {
    
        @Override
        public void accept(Integer value) {
            accept(value.intValue());
        }
    
        @Override
        public void accept(int value) {
            // todo
        }
    }
    

    注意:是Class Adapter Design Pattern的用法。

    那么您可以将IntConsumerAdapter 用作Consumer&lt;Integer&gt;IntConsumer,例如:

    Consumer<? extends Integer> consumer1 = new IntConsumerAdapter();
    IntConsumer consumer2 = new IntConsumerAdapter();
    

    Sink.OfInt 是 jdk-8 中Class Adapter Design Pattern 的具体用法。Sink.OfInt#accept(Integer) 的缺点显然是 JVM 在接受 null 值时会抛出 NullPointerException,这就是为什么 @987654361 @ 是 package 可见的。

    189 接口 OfInt 扩展 SinkInteger>, IntConsumer {
    190 @Override
    @ 987654329@ void 接受(int 值);
    193@Override
    194 默认 void 接受(Integer i) {
    195 if (Tripwire.ENABLED)
    196 Tripwire.trip(getClass(), "{0} 调用 Sink.OfInt.accept(Integer)");
    197 accept(i.intValue());
    198 }
    199 }

    我发现如果传递像IntConsumerAdapter 这样的消费者,为什么需要将Consumer&lt;Integer&gt; 转换为IntConsumer

    一个原因是当我们使用Consumer 来接受int 时,编译器需要将其自动装箱为Integer。在accept(Integer) 方法中,您需要手动将Integer 拆箱为int。 换句话说,每个accept(Integer) 都会为装箱/拆箱执行 2 个额外的操作。它需要提高性能,所以它在算法库中做了一些特殊的检查。

    另一个原因是重用一段代码。 OfInt#forEachRemaining(Consumer) 的主体是应用Adapter Design Pattern 以重用OfInt#forEachRenaming(IntConsumer) 的一个很好的例子。

    default void forEachRemaining(Consumer<? super Integer> action) {
        if (action instanceof IntConsumer) {
        //   action's implementation is an example of Class Adapter Design Pattern
        //                                   |
            forEachRemaining((IntConsumer) action);
        }
        else {
        //  method reference expression is an example of Object Adapter Design Pattern
        //                                        |
            forEachRemaining((IntConsumer) action::accept);
        }
    }
    

    【讨论】:

    • 该接口已经有“forEachRemaining”,它采用 IntConsumer,怎么可能用实现 IntConsumer 的对象调用“forEachRemaining(Consumer super Integer> action)”?除非调用者专门将其投射给消费者,但为什么呢?
    • @tsolakp 因为IntStream 不是Consumer&lt;Integer&gt;。它们位于不同的类层次结构中。但在这种情况下,您可以使用适配器模式将 Consumer&lt;Integer&gt; 适配为 IntConsumer
    【解决方案2】:

    因为实现类可能同时实现这两个接口。

    将任何类型转换为任何接口类型都是合法的,只要传递的对象可能实现目标接口。当源类型是未实现接口的最终类​​时,或者当它可以被证明具有导致相同擦除的不同类型参数化时,这在编译时已知为假。在运行时,如果对象没有实现接口,你会得到一个ClassCastException。在尝试强制转换之前检查 instanceof 可以避免异常。

    来自 Java 语言规范,5.5.1: Reference Type Casting

    5.5.1 引用类型转换 给定一个编译时引用类型 S (source) 和一个编译时引用类型 T(目标),如果没有发生编译时错误,则存在从 S 到 T 的强制转换 由于以下规则。

    ...

    • 如果 T 是接口类型: – 如果 S 不是最终类(第 8.1.1 节),那么,如果存在 T 的超类型 X 和 S 的超类型 Y,那么 X 和 Y 都是可证明不同的参数化类型,并且X 和 Y 相同,会出现编译时错误。

    否则,转换在编译时总是合法的(因为即使 S 没有实现 T,S 的子类也可能)。

    【讨论】:

    • 谢谢。在 lambda 的情况下会发生什么?它只是从右侧推断类型。
    • 对,关键之一是函数式接口类型的对象不一定对应lambda表达式。
    • @AndriiAbramov,由推断类型 Consumer&lt;? super Integer&gt; 的 lambda 表达式产生的对象将不是 IntConsumer 的实例。在这种情况下,您提供的方法中的instanceof 表达式将评估为false,结果是该方法没有可观察到的效果。
    【解决方案3】:

    请注意,您可以在不查看源代码的情况下发现此行为,只需查看the official API documentation,您已经链接了自己:

    实施要求:

    如果操作是IntConsumer 的实例,则将其转换为IntConsumer 并传递给forEachRemaining(java.util.function.IntConsumer);否则,该动作将通过装箱IntConsumer 的参数来适应IntConsumer 的实例,然后传递给forEachRemaining(java.util.function.IntConsumer)

    所以无论哪种情况,都会调用forEachRemaining(IntConsumer),这是实际的实现方法。但在可能的情况下,将省略装箱适配器的创建。原因是Spliterator.OfInt 也是Spliterator&lt;Integer&gt;,它只提供forEachRemaining(Consumer&lt;Integer&gt;) 方法。特殊行为允许平等对待通用 Spliterator 实例及其原始 (Spliterator.OfPrimitive) 对应物,并自动选择最有效的方法。

    正如其他人所说,您可以使用普通类实现多个接口。此外,如果您创建辅助类型,例如,您可以使用 lambda 表达式实现多个接口,例如

    interface UnboxingConsumer extends IntConsumer, Consumer<Integer> {
        public default void accept(Integer t) {
            System.out.println("unboxing "+t);
            accept(t.intValue());
        }
    }
    public static void printAll(BaseStream<Integer,?> stream) {
        stream.spliterator().forEachRemaining((UnboxingConsumer)System.out::println);
    }
    public static void main(String[] args) {
        System.out.println("Stream.of(1, 2, 3):");
        printAll(Stream.of(1, 2, 3));
        System.out.println("IntStream.range(0, 3)");
        printAll(IntStream.range(0, 3));
    }
    
    Stream.of(1, 2, 3):
    unboxing 1
    1
    unboxing 2
    2
    unboxing 3
    3
    IntStream.range(0, 3)
    0
    1
    2
    

    【讨论】:

    • 干得好。解释得更详细。它重视投票。
    • @holi-java:真是巧合,您刚刚添加到答案中的 JRE 的 Sink.OfIntUnboxingConsumer 的真实示例。
    • 是的,先生。 OfInt.**(Consumer)UnboxingConsumer 的一个例子,但我说的是另一种情况。我的第一个答案是解决他的ClassCastException 问题。过了一会儿,我的回答又多了一个观点。
    猜你喜欢
    • 1970-01-01
    • 2012-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-03
    • 2014-07-13
    • 2011-09-17
    相关资源
    最近更新 更多