为什么Java 8中的新java.util.Arrays方法不会为所有基元类型重载?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (1)
  • 关注 (0)
  • 查看 (9)

我正在评论Java 8的API更改,并且我注意到,java.util.Arrays对于所有基元,新方法不会重载。我注意到的方法是:

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

intlong并且double可能是最广泛使用的原语,所以如果他们必须限制API,他们会选择这三个,但为什么他们必须限制API?

提问于
用户回答回答于

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

Java 8中为什么会有Interface Pollution ?

例如,在如C#语言,有一组预定义的函数类型接受任何数量的参数与可选的返回类型的(函数功能操作每一个上升到不同类型的16个参数T1T2T3,..., T16),但在JDK 8中,我们拥有的是一组不同的函数接口,具有不同的名称和不同的方法名称,其抽象方法表示众所周知的函数库(即无限制,一元,二元,三元等)的子集。然后,我们处理原始类型的案例爆发,甚至还有其他场景导致更多功能性接口爆炸。

类型擦除问题

所以,从某种意义上讲,这两种语言都会遇到某种形式的界面污染(或C#代表污染)。唯一的区别是在C#中它们都具有相同的名称。在Java中,遗憾的是,由于类型擦除,有什么区别Function<T1,T2>Function<T1,T2,T3>Function<T1,T2,T3,...Tn>左右,显然,我们不能简单地命名他们都以同样的方式,我们必须拿出所有可能类型的功能组合创意的名字。

不要认为专家组没有为这个问题而挣扎。用莱布达邮件列表中的Brian Goetz的话来说:

[...]作为一个例子,我们来看一下函数类型。在devoxx提供的lambda strawman具有函数类型。我坚持要删除它们,这让我不受欢迎。但是我对函数类型的反对并不是我不喜欢函数类型 - 我喜欢函数类型 - 但是函数类型与Java类型系统的一个现有方面进行了很好的对抗,擦除。擦除的功能类型是两个世界中最差的。所以我们从设计中删除了这个。 但我不愿意说“Java永远不会有函数类型”(尽管我认识到Java可能永远不会有函数类型)。我相信为了获得函数类型,我们必须首先处理擦除。这可能,也可能不可能。但是在一个具体化结构类型的世界里,函数类型开始变得更加有意义[...]

这种方法的一个优点是我们可以使用接受尽可能多的参数的方法来定义我们自己的接口类型,并且我们可以使用它们来创建lambda表达式和方法引用。换句话说,我们有能力用更多的新功能界面来污染世界。我们也可以创建lambda表达式,甚至可以在早期版本的JDK中为接口创建lambda表达式,也可以为我们自己定义类似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)背后的哲学充满了讨厌的折衷。一方面,它的代码重复性很差,界面污染等等。另一方面,对盒装操作的任何类型的算术都很糟糕,并且没有减少整数的故事是非常可怕的。所以我们处于一个艰难的角落,我们正在努力不让它变得更糟。 诀窍#1没有让它更糟糕的是:我们不是做所有八种基本类型。我们在做int,long和double; 所有其他人都可以通过这些模拟。可以说我们也可以摆脱int,但我们不认为大多数Java开发人员已经准备好了。是的,会有角色的呼叫,答案是“坚持它在一个int。” (每个专业都预计到JRE足迹约100K。) 诀窍#2是:我们使用原始流来公开在原始域中最好完成的事情(排序,缩减),但不尝试复制在盒装域中可以做的所有事情。例如,没有IntStream.into(),正如Aleksey指出的那样。(如果有的话,下一个问题将是“IntCollection?IntArrayList?IntConcurrentSkipListMap?)的意图是许多流可能开始作为参考流,最终作为原始流,但不是反之亦然。减少了所需的转换次数(例如,对于int - > T,map没有重载,int没有对function进行特化 - > T等等)[...]

我们可以看到,这对专家组来说是一个困难的决定。我认为很少人会同意这很酷,而且我们大多数人很可能会认为这是必要的。

检查异常问题

有第三种驱动力可能使事情变得更糟,并且事实上Java支持两种类型的异常:checked和unchecked。编译器要求我们处理或显式声明已检查的异常,但对于未经检查的异常不需要任何处理。因此,这会产生一个有趣的问题,因为大多数功能接口的方法签名没有声明抛出任何异常。所以,例如,这是不可能的:

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

它不能完成,因为该write操作会抛出一个检查异常(即IOException),但Consumer方法的签名没有声明它抛出任何异常。所以,解决这个问题的唯一办法就是创建更多的接口,一些声明异常,一些不(或者在语言层面为异常透明提供另一种机制。再次,为了让事情“不那么糟糕”,专家小组决定在这种情况下什么也不做。

用莱布达邮件列表中的Brian Goetz的话来说:

[...]是的,你必须提供你自己的特殊SAMs。但是,lambda转换可以很好地与他们一起工作。 EG为此讨论了额外的语言和图书馆支持,并最终认为这是一种糟糕的成本/收益折衷。 基于库的解决方案会导致SAM类型爆发两次(特殊与非特殊),这与现有的原始专业化组合爆炸非常不利。 可用的基于语言的解决方案是复杂性/价值权衡的失败者。虽然有一些替代解决方案我们将继续探索 - 尽管显然不是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));

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

总之,我们可以看到专家组在设计问题上挣扎。保持向后兼容性的需求,要求或限制使事情变得困难,那么我们还有其他重要的条件,如缺少值类型,类型擦除和检查异常。如果Java拥有第一个并缺少其他两个,那么JDK 8的设计可能会有所不同。所以,我们都必须明白,这些都是很多折衷的难题,EG必须在某个地方划出界限并作出决定。

扫码关注云+社区