我正在审核Java 8的API更改,我注意到java.util.Arrays
中的新方法没有为所有原语重载。我注意到的方法是:
目前,这些新方法仅处理int
,long
和double
原语。
int
,long
和double
可能是使用最广泛的原语,因此如果他们必须限制API,他们会选择这三个,这是有意义的,但为什么呢?他们必须限制API吗?
答案 0 :(得分:79)
为了解决整个问题,而不仅仅是这个特定场景,我想我们都想知道......
例如,在像C#这样的语言中,有一组预定义的函数类型接受任意数量的参数,并带有可选的返回类型(Func和Action,每个参数最多可达16个参数不同类型T1
,T2
,T3
,...,T16
),但在JDK 8中我们拥有的是一组不同的功能接口,其中包含不同的名称和不同的方法名称,其抽象方法代表众所周知的function arities的子集(即nullary,unary,binary,trinary等)。然后我们处理原始类型的案例爆炸,甚至还有其他场景导致更多功能接口的爆炸。
因此,在某种程度上,两种语言都受到某种形式的界面污染(或代表C#中的污染)。唯一的区别是在C#中它们都具有相同的名称。遗憾的是,在Java中,由于type erasure,Function<T1,T2>
与Function<T1,T2,T3>
或Function<T1,T2,T3,...Tn>
之间没有区别,所以很明显,我们不能简单地将它们全部命名为同样的方式,我们必须为所有可能类型的功能组合提出创意名称。
不要认为专家组没有解决这个问题。用Brian lambda mailing list中的Brian Goetz的话来说:
[...]作为一个例子,让我们采用函数类型。 lambda devoxx提供的稻草人有功能类型。我坚持要删除 他们,这让我不受欢迎。但我反对功能类型 不是我不喜欢功能类型 - 我喜欢功能类型 - 但是这些功能类型与现有的方面有很大关系 Java类型系统,擦除。删除的功能类型是最糟糕的 这两个世界。所以我们从设计中删除了它。
但我不愿意说#34; Java永远不会有功能类型&#34; (虽然我认识到Java可能永远不会有函数类型。)I 相信为了获得功能类型,我们必须先处理 擦除。这可能是,也可能是不可能的。但在一个世界 具体的结构类型,功能类型开始做更多 感觉[...]
这种方法的一个优点是我们可以使用接受尽可能多的参数的方法定义我们自己的接口类型,并且我们可以使用它们来创建我们认为合适的lambda表达式和方法引用。换句话说,我们有污染世界的力量甚至更多的新功能接口。此外,我们甚至可以为早期版本的JDK中的接口创建lambda表达式,也可以为定义SAM类型的早期版本的API创建lambda表达式。因此,现在我们有权使用Runnable
和Callable
作为功能接口。
但是,这些界面变得更难以记忆,因为它们都有不同的名称和方法。
仍然,我是其中一个想知道为什么他们没有像Scala那样解决问题,定义Function0
,Function1
,Function2
等接口, FunctionN
。也许,我能提出的唯一论点就是他们希望最大化在早期版本的API中为接口定义lambda表达式的可能性。如前所述。
所以,显然类型擦除是这里的一个驱动力。但是如果你是其中一个想知道为什么我们还需要所有这些具有相似名称和方法签名的附加功能接口,并且唯一不同的是使用原始类型,那么让我提醒你,在Java中我们也 lack of value types就像C#这样的语言。这意味着我们的泛型类中使用的泛型类型只能是引用类型,而不是基本类型。
换句话说,我们无法做到这一点:
List<int> numbers = asList(1,2,3,4,5);
但我们确实可以这样做:
List<Integer> numbers = asList(1,2,3,4,5);
然而,第二个例子导致包装对象从原始类型来回打包和拆箱的成本。在处理原始值集合的操作中,这可能变得非常昂贵。因此,专家组决定创建接口爆炸来处理不同的场景。使事情变得更糟糕&#34;他们决定只处理三种基本类型:int,long和double。
中引用Brian Goetz的话[...]更一般地说:专业化背后的哲学 原始流(例如,IntStream)充满了令人讨厌的权衡。 一方面,它有很多丑陋的代码重复,界面 另一方面,对盒装操作的任何类型的算术 糟透了,没有减少in story的故事会很糟糕。 所以我们处在一个艰难的角落,我们一直试图不让它变得更糟。
不要让情况变得更糟的诀窍是:我们没有做到全部八 原始类型。我们做int,long和double;所有其他人 可以通过这些来模拟。可以说我们也可以摆脱int,但是 我们认为大多数Java开发人员都没有为此做好准备。是的,那里 将调用Character,答案是&#34;将其粘贴在一个int中。&#34; (每个专业化预计到JRE足迹约100K。)
技巧#2是:我们使用原始流来暴露事物 最好在原始域(排序,减少)完成但不尝试 复制您在盒装域中可以执行的所有操作。例如, 正如阿列克谢指出的那样,没有IntStream.into()。 (如果有的话, 接下来的问题是&#34; IntCollection在哪里? IntArrayList? IntConcurrentSkipListMap?)意图是许多流可能以 引用流并最终作为原始流,但反之亦然。 没问题,这减少了所需的转换次数(例如,没有 int的映射重载 - &gt; T,没有专门的int函数 - &GT; T等等)[...]
我们可以看到这对专家组来说是一个艰难的决定。我认为很少有人会同意这很酷,我们大多数人都很同意这是必要的。
第三个驱动力可能会使事情变得更糟,而事实是Java支持两种类型的例外:已检查和未检查。编译器要求我们处理或显式声明已检查的异常,但对于未经检查的异常,它不需要任何内容。因此,这会产生一个有趣的问题,因为大多数功能接口的方法签名都不会声明抛出任何异常。所以,例如,这是不可能的:
Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error
无法完成,因为write
操作会抛出一个已检查的异常(即IOException
),但Consumer
方法的签名并未声明它会抛出任何异常。因此,解决这个问题的唯一方法就是创建更多的接口,一些声明异常,一些不起作用(或者在exception transparency的语言层面提出另一种机制。再次,制作东西&#34更糟糕的是,在这种情况下,专家组决定不做任何事情。
用lambda mailing list中的Brian Goetz的话说:
[...]是的,您必须提供自己的特殊SAM。但是之后 lambda转换对它们很好。
EG讨论了额外的语言和图书馆支持 问题,最后觉得这是一个不好的成本/收益 折衷。
基于库的解决方案导致SAM类型爆发2倍(特殊情况 vs not),它与现有的组合爆炸相互作用很严重 原始专业化。
基于语言的可用解决方案是a的输家 复杂性/价值权衡。虽然有一些选择 我们将继续探索的解决方案 - 尽管显然不是 8,也许不是9。
与此同时,你有工具可以做你想做的事。我明白了 你更喜欢我们为你提供最后一英里(其次,你的 请求实际上是一个薄薄的请求&#34;为什么不给你 已经检查过的异常&#34;),但我认为当前状态允许 你完成了你的工作。 [...]
因此,开发人员可以根据具体情况制定甚至更多的界面爆炸来处理这些问题:
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),当我们获得Support for Value Types in Java和Reification时,我们将能够摆脱(或至少不再需要使用)这些多个接口中的一些。 / p>
总之,我们可以看到专家组在解决几个设计问题时遇到了困难。保持向后兼容性的需要,要求或约束使事情变得困难,然后我们还有其他重要条件,例如缺少值类型,类型擦除和检查异常。如果Java有第一个而缺少其他两个,那么JDK 8的设计可能会有所不同。所以,我们都必须明白,这些是很多权衡的难题,而EG必须在某处划一条线并作出决定。