为什么 Java 8 中的新 java.util.Arrays 方法没有对所有基元类型进行重载?

2022-08-31 17:36:50

我正在查看Java 8的API更改,我注意到其中的新方法对于所有基元都没有重载。我注意到的方法是:java.util.Arrays

目前这些新方法只处理 、 和基元。intlongdouble

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


答案 1

为了从整体上解决这些问题,而不仅仅是这个特定的场景,我想我们都想知道......

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

例如,在像C#这样的语言中,有一组预定义的函数类型接受任意数量的参数,并具有可选的返回类型(FuncAction每个参数最多有16个不同类型的参数,,,...,但在JDK 8中,我们拥有的是一组不同的功能接口,具有不同的名称不同的方法名称, 其抽象方法表示已知函数的子集(即零,一元,二元,三元等)。然后,我们有处理基元类型的案例的爆炸式增长,甚至还有其他场景导致更多功能接口的爆炸式增长。T1T2T3T16

类型擦除问题

因此,在某种程度上,这两种语言都遭受了某种形式的界面污染(或C#中的委托污染)。唯一的区别是,在 C# 中,它们都具有相同的名称。不幸的是,在Java中,由于类型擦除,和或之间没有区别,所以很明显,我们不能简单地以相同的方式命名它们,我们必须为所有可能的函数组合类型提出创造性的名称。有关这方面的进一步参考,请参阅Brian Goetz如何获得我们的泛型Function<T1,T2>Function<T1,T2,T3>Function<T1,T2,T3,...Tn>

不要以为专家组没有为这个问题而苦苦挣扎。用Brian Goetz在lambda邮件列表中的话来说:

[...]作为一个示例,让我们以函数类型为例。devoxx提供的lambda稻草人具有函数类型。我坚持要我们删除它们,这使我不受欢迎。但我对函数类型的反对意见并不是我不喜欢函数类型——我喜欢函数类型——而是函数类型与Java类型系统的现有方面——擦除——进行了激烈的斗争。擦除的函数类型是两个世界中最糟糕的。所以我们从设计中删除了它。

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

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

但是,这些接口变得更加难以记忆,因为它们都具有不同的名称和方法。

不过,我还是其中之一,想知道为什么他们没有像在Scala中那样解决问题,定义诸如,,,...,之类的接口。也许,我能提出的唯一论点是,他们希望最大限度地利用在早期版本的API中为接口定义lambda表达式的可能性,如前所述。Function0Function1Function2FunctionN

缺少值类型问题

因此,显然类型擦除是这里的一个驱动力。但是,如果您想知道为什么我们还需要所有这些具有相似名称和方法签名的附加功能接口,并且其唯一的区别是使用基元类型,那么让我提醒您,在Java中,我们也缺少像C#这样的语言中的值类型。这意味着泛型类中使用的泛型类型只能是引用类型,而不能是基元类型。

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

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

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

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

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

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

[...]更一般地说:拥有专门的原始流(例如,IntStream)背后的哲学充满了令人讨厌的权衡。一方面,它有很多丑陋的代码重复,接口污染等。另一方面,任何一种关于盒装运算的算术都很糟糕,并且没有减少整数的故事将是可怕的。因此,我们处于一个艰难的角落,我们试图不让它变得更糟。

不让它变得更糟的诀窍#1是:我们没有做所有八个基元类型。我们正在做int,long和double;所有其他的都可以通过这些来模拟。可以说,我们也可以摆脱int,但我们认为大多数Java开发人员还没有准备好。是的,会有对角色的呼唤,答案是“把它贴在一个int中”。(每个特化都投影到 JRE 足迹约 100K。

诀窍#2是:我们使用原始流来公开在原始域中最好完成的事情(排序,缩减),而不是尝试复制您在盒装域中可以做的所有事情。例如,没有IntStream.into(),正如Aleksey指出的那样。(如果有,下一个问题是“IntCollection在哪里?IntArrayList?IntConcurrentSkipListMap?)目的是许多流可能从参考流开始,最终成为原始流,但反之亦然。没关系,这减少了所需的转换次数(例如,int -> T 没有映射的重载,int -> T 没有函数的特化,等等。[...]

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

有关该主题的进一步参考,您可能需要阅读John Rose,Brian Goetz和Guy Steele的The State of Value Types

已检查的异常问题

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

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

这不能完成,因为该操作会引发一个选中的异常(即 ),但该方法的签名根本不声明它会引发任何异常。因此,解决这个问题的唯一方法是创建更多的接口,一些声明异常,一些不声明(或者在语言级别提出另一种机制来实现异常透明。同样,为了使事情“不那么糟糕”,专家组决定在这种情况下不采取任何行动。writeIOExceptionConsumer

用Brian Goetz在lambda邮件列表中的话来说:

[...]是的,您必须提供自己的特殊 SAM。但是,lambda转换将很好地与它们一起工作。

EG讨论了针对此问题的其他语言和库支持,并最终认为这是一个糟糕的成本/收益权衡。

基于库的解决方案导致 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必须在某个地方划一条线并做出决定。


答案 2