java - 为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载?

标签 java arrays java-8

我正在查看 Java 8 的 API 更改,我注意到 java.util.Arrays 中的新方法不会为所有原语重载。我注意到的方法是:

  • parallelSetAll
  • parallelPrefix
  • spliterator
  • stream

  • 目前这些新方法只处理 int , long , 和 double原语。
    int , long , 和 double可能是使用最广泛的原语,所以如果他们必须限制 API,他们会选择这三个是有道理的,但为什么他们必须限制 API?

    最佳答案

    为了从整体上解决这些问题,而不仅仅是这个特定的场景,我想我们都想知道......
    为什么 Java 8 中存在接口(interface)污染
    例如,在像 C# 这样的语言中,有一组预定义的函数类型,它们接受任意数量的参数和可选的返回类型( FuncAction 每个都有多达 16 个不同类型的参数 T1T2T3 ,... , T16 ),但在 JDK 8 中我们拥有的是一组不同的功能接口(interface),具有不同的名称和不同的方法名称,并且其抽象方法表示众所周知的 function arities(即空值、一元、二元、三元等)的子集)。然后我们有处理原始类型的案例爆炸式增长,甚至还有其他场景导致更多功能接口(interface)的爆炸式增长。
    类型删除问题
    因此,在某种程度上,这两种语言都遭受某种形式的接口(interface)污染(或 C# 中的委托(delegate)污染)。唯一的区别是在 C# 中它们都具有相同的名称。在 Java 中,不幸的是,由于 type erasureFunction<T1,T2>Function<T1,T2,T3>Function<T1,T2,T3,...Tn> 之间没有区别,所以显然,我们不能简单地以相同的方式命名它们,我们必须为所有可能的函数组合类型想出创造性的名称.有关这方面的进一步引用,请参阅 Brian Goetz 的 How we got the generics we have
    不要以为专家组没有在这个问题上挣扎。用 lambda mailing list 中 Brian Goetz 的话来说:

    [...] As a single example, let's take function types. The lambda strawman offered at devoxx had function types. I insisted we remove them, and this made me unpopular. But my objection to function types was not that I don't like function types -- I love function types -- but that function types fought badly with an existing aspect of the Java type system, erasure. Erased function types are the worst of both worlds. So we removed this from the design.

    But I am unwilling to say "Java never will have function types" (though I recognize that Java may never have function types.) I believe that in order to get to function types, we have to first deal with erasure. That may, or may not be possible. But in a world of reified structural types, function types start to make a lot more sense [...]


    这种方法的一个优点是我们可以定义我们自己的接口(interface)类型,方法接受任意数量的参数,我们可以使用它们来创建我们认为合适的 lambda 表达式和方法引用。换句话说,我们有能力用更多新的功能界面来污染世界。此外,我们甚至可以为早期版本的 JDK 中的接口(interface)或我们自己定义了此类 SAM 类型的早期版本的 API 创建 lambda 表达式。所以现在我们有能力使用 RunnableCallable 作为功能接口(interface)。
    然而,这些接口(interface)变得更加难以内存,因为它们都有不同的名称和方法。
    尽管如此,我还是想知道为什么他们没有像在 Scala 中那样解决问题的人之一,定义了 Function0Function1Function2 、...、 FunctionN 之类的接口(interface)。也许,我能提出的唯一论点是,他们希望最大限度地为之前提到的 API 早期版本中的接口(interface)定义 lambda 表达式的可能性。
    缺少值类型问题
    因此,显然类型删除是这里的一种驱动力。但是,如果您想知道为什么我们还需要所有这些具有相似名称和方法签名的附加功能接口(interface),并且它们唯一的区别是使用原始类型,那么让我提醒您,在 Java 中我们也 lack of value types 就像在 a像 C# 这样的语言。这意味着在我们的泛型类中使用的泛型类型只能是引用类型而不是原始类型。
    换句话说,我们不能这样做:
    List<int> numbers = asList(1,2,3,4,5);
    
    但我们确实可以这样做:
    List<Integer> numbers = asList(1,2,3,4,5);
    
    但是,第二个示例会产生在原始类型之间来回装箱和拆箱包装对象的成本。这在处理原始值集合的操作中会变得非常昂贵。因此,专家组决定创建这种爆炸式的接口(interface)来处理不同的场景。为了让事情“不那么糟糕”,他们决定只处理三种基本类型:int、long 和 double。
    lambda mailing list 中引用 Brian Goetz 的话:

    [...] More generally: the philosophy behind having specialized primitive streams (e.g., IntStream) is fraught with nasty tradeoffs. On the one hand, it's lots of ugly code duplication, interface pollution, etc. On the other hand, any kind of arithmetic on boxed ops sucks, and having no story for reducing over ints would be terrible. So we're in a tough corner, and we're trying to not make it worse.

    Trick #1 for not making it worse is: we're not doing all eight primitive types. We're doing int, long, and double; all the others could be simulated by these. Arguably we could get rid of int too, but we don't think most Java developers are ready for that. Yes, there will be calls for Character, and the answer is "stick it in an int." (Each specialization is projected to ~100K to the JRE footprint.)

    Trick #2 is: we're using primitive streams to expose things that are best done in the primitive domain (sorting, reduction) but not trying to duplicate everything you can do in the boxed domain. For example, there's no IntStream.into(), as Aleksey points out. (If there were, the next question(s) would be "Where is IntCollection? IntArrayList? IntConcurrentSkipListMap?) The intention is many streams may start as reference streams and end up as primitive streams, but not vice versa. That's OK, and that reduces the number of conversions needed (e.g., no overload of map for int -> T, no specialization of Function for int -> T, etc.) [...]


    我们可以看到,这对专家组来说是一个艰难的决定。我认为很少有人会同意这是优雅的,但我们大多数人很可能会同意这是必要的。
    有关该主题的进一步引用,您可能需要阅读 John Rose、Brian Goetz 和 Guy Steele 的 The State of Value Types
    检查异常问题
    还有第三个可能使事情变得更糟的驱动力,那就是 Java 支持两种类型的异常:已检查和未检查。编译器要求我们处理或显式声明已检查的异常,但对于未检查的异常则不需要。所以,这就产生了一个有趣的问题,因为大多数函数式接口(interface)的方法签名没有声明抛出任何异常。因此,例如,这是不可能的:
    Writer out = new StringWriter();
    Consumer<String> printer = s -> out.write(s); //oops! compiler error
    
    这是无法完成的,因为 write 操作抛出了一个已检查的异常(即 IOException ),但 Consumer 方法的签名根本没有声明它抛出任何异常。所以,这个问题的唯一解决方案是创建更多的接口(interface),一些声明异常,一些不声明(或者在语言级别为 exception transparency 提出另一种机制。再次,让专家组“不那么糟糕”决定在这种情况下什么都不做。
    lambda mailing list 中 Brian Goetz 的话来说:

    [...] Yes, you'd have to provide your own exceptional SAMs. But then lambda conversion would work fine with them.

    The EG discussed additional language and library support for this problem, and in the end felt that this was a bad cost/benefit tradeoff.

    Library-based solutions cause a 2x explosion in SAM types (exceptional vs not), which interact badly with existing combinatorial explosions for primitive specialization.

    The available language-based solutions were losers from a complexity/value tradeoff. Though there are some alternative solutions we are going to continue to explore -- though clearly not for 8 and probably not for 9 either.

    In the meantime, you have the tools to do what you want. I get that you prefer we provide that last mile for you (and, secondarily, your request is really a thinly-veiled request for "why don't you just give up on checked exceptions already"), but I think the current state lets you get your job done. [...]


    因此,由我们开发人员来制作更多的界面爆炸,以逐案处理这些问题:
    interface IOConsumer<T> {
       void accept(T t) throws IOException;
    }
    
    static<T> Consumer<T> exceptionWrappingBlock(IOConsumer<T> b) {
       return e -> {
        try { b.accept(e); }
        catch (Exception ex) { throw new RuntimeException(ex); }
       };
    }
    
    为了做到:
    Writer out = new StringWriter();
    Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));
    
    或许,将来当我们获得 Support for Value Types in Java 和 Reification 时,我们将能够摆脱(或至少不再需要使用)这些多个接口(interface)中的一些。
    总之,我们可以看到专家组在几个设计问题上苦苦挣扎。保持向后兼容性的需要、要求或约束使事情变得困难,然后我们还有其他重要条件,例如缺少值类型、类型删除和检查异常。如果 Java 拥有第一个而缺少其他两个,那么 JDK 8 的设计可能会有所不同。所以,我们都必须明白,这些都是需要权衡的难题,EG 必须在某处划清界限并做出决定。

    关于java - 为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22918847/

    相关文章:

    java - 任务托盘通知气球事件?

    java - Collections.shuffle的有效性

    Java8 Lambda 和异常

    java - 用于保存并随后组合稀疏数据的数据结构

    java - 如何计算与 Streams 的谓词匹配的元素数量?

    java - 二维高架瓦片生成

    java - 为集合中的每个元素调用 JsonGenerator 方法

    objective-c - 将键的值插入 NSMutableArray

    java - 如何使用二进制搜索来查找具有特定权重的数组中的第一个元素(不同数组中的另一个元素)?

    php - 在mysql上显示单个结果