当前位置: 首页 > 面试题库 >

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

濮阳茂材
2023-03-14
问题内容

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

  • parallelSetAll
  • parallelPrefix
  • 分离器

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

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


问题答案:

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

为什么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必须在某处划界并做出决定。



 类似资料:
  • 问题内容: 为什么大多数其他数据类型都没有Java的String基本类型? 问题答案: 字符串是一个对象,根本不是原始类型,只是一个字符数组。James Gosling的访谈 摘录摘述了Java中根本存在原始类型的原因,这很有趣。 Bill Venners: Java为什么会有原始类型?为什么不是所有事物都只是一个对象? James Gosling: 完全是效率问题。有各种各样的人已经建立了以in

  • 问题内容: 如果有Wrapper类使Java成为纯面向对象的语言,那么为什么会有可在Java中使用的Primitive数据类型呢? 问题答案: 为了效率。基本类型的变量直接包含值。非基本类型的变量是引用,引用存储在内存中其他位置的对象。 每次您需要使用包装器类型的值时,JVM都需要在内存中查找对象以获取该值。对于原始类型,这不是必需的,因为变量本身包含值,而不是对包含该值的对象的引用。 但是,这不

  • 在这部分内容的第一章中,我们提到了设置原型的现代方法。 __proto__ 被认为是过时且不推荐使用的(deprecated),这里的不推荐使用是指 JavaScript 规范中规定,proto 必须仅在浏览器环境下才能得到支持。 现代的方法有: Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述

  • 问题内容: 这确实是一个好奇心,而不是一个问题。 为什么类没有方法?现在看来似乎应该当你认为它有一个事实,,等方法。 我知道您可以执行以下操作: 但是为什么没有方法呢? 问题答案: 原因是Scanner类旨在读取以空格分隔的令牌。这是包装基础输入流的便捷类。在扫描仪之前,您所能做的只是读取单个字节,如果要读取单词或行,这将是一个很大的麻烦。使用Scanner,您可以传入System.in,它会执行

  • 使用泛型,原始类型不能作为类型参数传递。 在下面给出的示例中,如果我们将int基本类型传递给box类,那么编译器会抱怨。 为了缓解这种情况,我们需要传递Integer对象而不是int基本类型。 例子 (Example) package com.wenjiangs; public class GenericsTester { public static void main(String[] a

  • 我正在为新手程序员编写一个库,所以我试图保持API尽可能干净。 我的库需要做的事情之一是对大量的ints或long集合执行一些复杂的计算。我的用户需要从许多场景和业务对象中计算这些值,因此我认为最好的方法是使用流来允许用户将业务对象映射到或,然后在收集器中计算这些计算。 所以与其能够做到 我必须提供这样的供应商、累加器和组合器: 这对我的新手用户来说太复杂了,而且很容易出错。 在使用或的同时,是否