一尘不染

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

java

我正在查看Java 8的API更改,并且注意到新的方法java.util.Arrays并未针对所有原语进行重载。我注意到的方法是:

目前,这些新的方法只能处理intlongdouble原语。

intlongdouble可能是使用最广泛的原语,因此,如果必须限制API,那么他们会选择这三个,这是有道理的,但是为什么必须限制API?


阅读 330

收藏
2020-09-08

共1个答案

一尘不染

为了解决整个问题,而不仅仅是这个特定情况,我想我们都想知道…

为什么Java 8中存在接口污染

例如,在如C#语言,有一组预定义的函数类型接受任何数量的参数与可选的返回类型的(函数功能操作每一个上升到不同类型的16个参数T1T2T3,…,
T16),但是在JDK 8中,我们拥有一组不同的功能接口,它们具有 不同的 名称和 不同的方法
名称,并且它们的抽象方法代表了众所周知的函数范围的子集(即,无效,一元,二进制,三元等)。然后,我们遇到了涉及原始类型的案例激增,甚至还有其他情况导致了更多功能接口的激增。

类型擦除问题

因此,从某种意义上讲,两种语言都遭受某种形式的界面污染(或C#中的委托污染)。唯一的区别是,在C#中,它们都具有相同的名称。不幸的是,在Java中,由于类型擦除Function<T1,T2>and和Function<T1,T2,T3>or
之间没有区别Function<T1,T2,T3,...Tn>,因此很显然,我们不能简单地以相同的方式命名它们,而必须为所有可能的功能组合类型想出创造性的名称。有关此内容的更多参考,请参阅Brian
Goetz的“
我们如何获得仿制药”

不要以为专家组没有解决这个问题。用lambda邮件列表中的Brian Goetz的话:

[…]作为一个单独的例子,让我们看一下函数类型。devoxx提供的lambda稻草人具有函数类型。我坚持要删除它们,这使我不受欢迎。但是我对函数类型的反对并非不是我不喜欢函数类型-
我喜欢函数类型-而是函数类型与Java类型系统的现有方面进行了激烈的对抗。擦除的函数类型在两个方面都是最糟糕的。因此,我们从设计中删除了此内容。

但是我不愿意说“
Java永远不会有函数类型”(尽管我认识到Java可能永远不会有函数类型。)我相信,要获得函数类型,我们必须首先处理擦除。那可能或不可能。但是,在一个结构化类型化的世界中,函数类型开始变得更加有意义[…]

这种方法的优点是,我们可以使用可接受的参数来定义自己的接口类型,并且可以使用它们创建合适的lambda表达式和方法引用。换句话说,我们有 能力
使用更多新的功能接口 来污染世界
。同样,我们甚至可以为JDK的早期版本中的接口或为定义此类SAM类型的我们自己的API的早期版本创建lambda表达式。因此,我们现在可以使用RunnableCallable作为功​​能接口。

但是,由于这些接口都有不同的名称和方法,因此很难记住。

不过,我那些不知道他们为什么没有解决的问题,如Scala中的一个,定义接口一样Function0Function1Function2,…,
FunctionN。也许,我唯一可以反对的论点是,他们希望最大程度地为之前提到的API版本中的接口定义lambda表达式的可能性。

缺乏价值类型问题

因此,显然类型擦除是这里的驱动力之一。但是,如果您是其中的一员,为什么我们还需要所有这些具有相似名称和方法签名的附加功能接口,而它们的唯一区别是使用原始类型,那么让我提醒您,在Java中我们

缺少诸如像C#这样的语言。这意味着在我们的泛型类中使用的泛型类型只能是引用类型,而不能是原始类型。

换句话说,我们不能这样做:

List<int> numbers = asList(1,2,3,4,5);

但是我们确实可以做到这一点:

List<Integer> numbers = asList(1,2,3,4,5);

但是,第二个示例会产生将包装对象从原始类型来回装箱和拆箱的成本。这在处理原始值集合的操作中可能变得非常昂贵。因此,专家组决定创建这种 爆炸式的界面
来应对不同的情况。为了使事情“更糟”,他们决定只处理三种基本类型:int,long和double。

lambda邮件列表中引用Brian Goetz的话:

[…]更笼统地说:拥有专门的原始流(例如IntStream)背后的哲学充满了令人讨厌的折衷。一方面,这有很多丑陋的代码重复,接口污染等。另一方面,盒装操作上的任何一种算法都糟透了,而没有减少整数的故事将是可怕的。因此,我们处在艰难的境地,我们正在努力不使其变得更糟。

不使情况变得更糟的第一招是:我们没有做所有八种原始类型。我们正在做int,long和double;所有其他的都可以用这些来模拟。可以说我们也可以摆脱int,但我们认为大多数Java开发人员都没有为此做好准备。是的,将会有针对性格的呼吁,而答案是“将其粘贴在一个整数中”。(每个专业化项目预计将占JRE的空间约100K。)

技巧2:我们正在使用原始流来公开在原始域中最好完成的事情(排序,归约),而不是尝试复制盒装域中可以做的所有事情。例如,正如Aleksey所指出的,没有IntStream.into()。(如果有的话,下一个问题将是“
IntCollection在哪里?IntArrayList?IntConcurrentSkipListMap?”。)意图是许多流可能以引用流开始,最终以原始流结束,但是反之则不然。减少所需的转换次数(例如,对于int->
T,没有映射重载,对于int-> T,没有函数的特殊化,等等)[…]

我们可以看到,对于专家组来说,这是一个艰难的决定。我认为很少有人会同意这是优雅的做法,但我们大多数人很可能会认为这是必要的。

有关该主题的更多参考,您可能需要阅读 John Rose,Brian Goetz和Guy
Steele撰写的《价值类型状况》

检查异常问题

第三种驱动因素 可能会使情况变得更糟
,这是Java支持两种异常的事实:检查和未检查。编译器要求我们处理或显式声明已检查的异常,但对于未检查的异常则不需要任何内容​​。因此,这会引起一个有趣的问题,因为大多数功能接口的方法签名都没有声明抛出任何异常。因此,例如,这是不可能的:

Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error

之所以不能这样做,是因为该write操作引发了一个检查过的异常(即IOException),但是Consumer方法的签名没有声明它根本引发了任何异常。因此,解决该问题的唯一方法是创建更多接口,一些接口声明异常,而另一些接口不声明(或者在语言级别上提供另一种机制,以确保异常透明。再次,使专家“变得更糟”小组决定在这种情况下不采取任何措施。

lambda邮件列表中的Brian Goetz的话:

[…]是的,您必须提供自己的特殊SAM。但是,然后lambda转换将与他们一起工作。

专家组讨论了针对此问题的其他语言和库支持,最后认为这是一个不好的成本/收益折衷方案。

基于库的解决方案导致SAM类型发生2倍的爆炸(异常与非爆炸),这与现有的组合爆炸(用于原始专业化)相互作用不良。

可用的基于语言的解决方案是复杂性/价值折衷的失败者。尽管有一些替代解决方案,我们将继续探索-尽管显然不是针对8个,也可能不是针对9个。

同时,您拥有执行所需任务的工具。我得到您的青睐,我们愿意为您提供最后一英里(其次,您的请求实际上是“为什么您不已经放弃受检查的异常”的薄弱要求),但是我认为当前状态允许您完成工作。[…]

因此,由开发人员自行决定是否制定 更多界面爆炸, 以根据具体情况进行处理:

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

也许将来,当我们获得Java和Reification中对值类型的支持时,我们将能够摆脱(或至少不再需要使用)这些多个接口。

总而言之,我们可以看到专家组在几个设计问题上苦苦挣扎。保持向后兼容性的需求,要求或约束使事情变得困难,然后我们还有其他重要条件,例如缺少值类型,类型擦除和检查异常。如果Java有第一个而缺少其他两个,则JDK
8的设计可能会有所不同。因此,我们所有人都必须理解,这些都是很多折衷的难题,而EG必须在某处划界并做出决定。

2020-09-08