为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载? [英] Why are new java.util.Arrays methods in Java 8 not overloaded for all the primitive types?

查看:42
本文介绍了为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm reviewing the API changes for Java 8 and I noticed that the new methods in java.util.Arrays are not overloaded for all primitives. The methods I noticed are:

目前这些新方法只处理 intlongdouble 原语.

Currently these new methods only handle int, long, and double primitives.

intlongdouble 可能是使用最广泛的原语,所以如果他们不得不限制 API他们会选择这三个,但他们为什么要限制 API?

int, long, and double are probably the most widely used primitives so it makes sense that if they had to limit the API that they would choose those three, but why did they have to limit the API?

推荐答案

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

To address the questions as a whole, and not just this particular scenario, I think we all want to know...

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

For instance, in a language like C#, there is a set of predefined function types accepting any number of arguments with an optional return type (Func and Action each one going up to 16 parameters of different types T1, T2, T3, ..., T16), but in the JDK 8 what we have is a set of different functional interfaces, with different names and different method names, and whose abstract methods represent a subset of well known function arities (i.e. nullary, unary, binary, ternary, etc). And then we have an explosion of cases dealing with primitive types, and there are even other scenarios causing an explosion of more functional interfaces.

因此,在某种程度上,这两种语言都遭受某种形式的接口污染(或 C# 中的委托污染).唯一的区别是在 C# 中它们都具有相同的名称.在 Java 中,不幸的是,由于 type擦除FunctionFunctionFunction,很明显,我们不能简单地以相同的方式命名它们,我们必须为所有可能的功能组合类型想出创造性的名称.有关这方面的进一步参考,请参阅我们如何获得我们拥有的泛型 作者:Brian Goetz.

So, in a way, both languages suffer from some form of interface pollution (or delegate pollution in C#). The only difference is that in C# they all have the same name. In Java, unfortunately, due to type erasure, there is no difference between Function<T1,T2> and Function<T1,T2,T3> or Function<T1,T2,T3,...Tn>, so evidently, we couldn't simply name them all the same way and we had to come up with creative names for all possible types of function combinations. For further reference on this, please refer to How we got the generics we have by Brian Goetz.

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

Don't think the expert group did not struggle with this problem. In the words of Brian Goetz in the lambda mailing list:

[...] 作为一个单一的例子,让我们以函数类型为例.拉姆达在devoxx 提供的strawman 有函数类型.我坚持我们删除他们,这让我不受欢迎.但我反对函数类型不是我不喜欢函数类型——我喜欢函数类型——但该函数类型与Java 类型系统,擦除.擦除的函数类型是最糟糕的两个世界.所以我们从设计中删除了这个.

[...] As a single example, let's take function types. The lambda strawman offered at devoxx had function types. I insisted we remove them, and this made me unpopular. But my objection to function types was not that I don't like function types -- I love function types -- but that function types fought badly with an existing aspect of the Java type system, erasure. Erased function types are the worst of both worlds. So we removed this from the design.

但我不愿意说Java 永远不会有函数类型";(虽然我认识到 Java 可能永远没有函数类型.)我相信为了得到函数类型,我们必须首先处理与擦除.这可能,也可能不可能.但在一个世界具体化的结构类型,函数类型开始变得更多感觉 [...]

But I am unwilling to say "Java never will have function types" (though I recognize that Java may never have function types.) I believe that in order to get to function types, we have to first deal with erasure. That may, or may not be possible. But in a world of reified structural types, function types start to make a lot more sense [...]

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

An advantage of this approach is that we can define our own interface types with methods accepting as many arguments as we would like, and we could use them to create lambda expressions and method references as we see fit. In other words, we have the power to pollute the world with yet even more new functional interfaces. Also, we can create lambda expressions even for interfaces in earlier versions of the JDK or for earlier versions of our own APIs that defined SAM types like these. And so now we have the power to use Runnable and Callable as functional interfaces.

然而,这些接口变得更加难以记忆,因为它们都有不同的名称和方法.

However, these interfaces become more difficult to memorize since they all have different names and methods.

尽管如此,我还是想知道为什么他们没有像在 Scala 中那样解决问题,定义像 Function0Function1Function2, ..., FunctionN.也许,我能提出的唯一论点是,他们希望最大限度地为之前提到的 API 早期版本中的接口定义 lambda 表达式.

Still, I am one of those wondering why they didn't solve the problem as in Scala, defining interfaces like Function0, Function1, Function2, ..., FunctionN. Perhaps, the only argument I can come up with against that is that they wanted to maximize the possibilities of defining lambda expressions for interfaces in earlier versions of the APIs as mentioned before.

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

So, evidently type erasure is one driving force here. But if you are one of those wondering why we also need all these additional functional interfaces with similar names and method signatures and whose only difference is the use of a primitive type, then let me remind you that in Java we also lack of value types like those in a language like C#. This means that the generic types used in our generic classes can only be reference types and not primitive types.

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

In other words, we can't do this:

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

但我们确实可以这样做:

But we can indeed do this:

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

但是,第二个示例会产生在原始类型之间来回装箱和拆箱包装对象的成本.这在处理原始值集合的操作中会变得非常昂贵.因此,专家组决定创建这个接口爆炸来处理不同的场景.让事情不那么糟糕"他们决定只处理三种基本类型:int、long 和 double.

The second example, though, incurs in the cost of boxing and unboxing of the wrapped objects back and forth from/to primitive types. This can become really expensive in operations dealing with collections of primitive values. So, the expert group decided to create this explosion of interfaces to deal with the different scenarios. To make things "less worse" they decided to only deal with three basic types: int, long and double.

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

Quoting the words of Brian Goetz in the lambda mailing list:

[...] 更笼统地说:拥有专业技术背后的哲学原始流(例如 IntStream)充满了令人讨厌的权衡.一方面,它有很多丑陋的代码重复,界面污染等另一方面,盒装操作上的任何算术糟透了,没有减少整数的故事会很糟糕.所以我们正处于艰难的境地,我们正在努力不让它变得更糟.

[...] More generally: the philosophy behind having specialized primitive streams (e.g., IntStream) is fraught with nasty tradeoffs. On the one hand, it's lots of ugly code duplication, interface pollution, etc. On the other hand, any kind of arithmetic on boxed ops sucks, and having no story for reducing over ints would be terrible. So we're in a tough corner, and we're trying to not make it worse.

不让事情变得更糟的技巧 #1 是:我们没有做所有八件事原始类型.我们正在做 int、long 和 double;所有其他人可以通过这些来模拟.可以说我们也可以摆脱 int,但是我们认为大多数 Java 开发人员还没有为此做好准备.是的,有将调用 Character,答案是将其放入 int".(每个专业化预计占 JRE 占用空间约 100K.)

Trick #1 for not making it worse is: we're not doing all eight primitive types. We're doing int, long, and double; all the others could be simulated by these. Arguably we could get rid of int too, but we don't think most Java developers are ready for that. Yes, there will be calls for Character, and the answer is "stick it in an int." (Each specialization is projected to ~100K to the JRE footprint.)

技巧#2 是:我们使用原始流来公开那些最好在原始域中完成(排序、归约)但不要尝试复制您可以在盒装域中执行的所有操作.例如,正如 Aleksey 指出的那样,没有 IntStream.into().(如果有,下一个问题是IntCollection 在哪里?数组列表?IntConcurrentSkipListMap?) 目的是许多流可以作为引用流并最终成为原始流,但反之亦然.没关系,这减少了所需的转换次数(例如,没有int 的 map 重载 ->T,没有对 int 函数的特化->T 等)[...]

Trick #2 is: we're using primitive streams to expose things that are best done in the primitive domain (sorting, reduction) but not trying to duplicate everything you can do in the boxed domain. For example, there's no IntStream.into(), as Aleksey points out. (If there were, the next question(s) would be "Where is IntCollection? IntArrayList? IntConcurrentSkipListMap?) The intention is many streams may start as reference streams and end up as primitive streams, but not vice versa. That's OK, and that reduces the number of conversions needed (e.g., no overload of map for int -> T, no specialization of Function for int -> T, etc.) [...]

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

We can see that this was a difficult decision for the expert group. I think few would agree that this is elegant, but most of us would most likely agree it was necessary.

有关该主题的进一步参考,您可能需要阅读 价值类型的状态 作者:John Rose、Brian Goetz 和 Guy Steele.

For further reference on the subject you may want to read The State of Value Types by John Rose, Brian Goetz, and Guy Steele.

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

There was a third driving force that could have made things even worse, and it is the fact that Java supports two types of exceptions: checked and unchecked. The compiler requires that we handle or explicitly declare checked exceptions, but it requires nothing for unchecked ones. So, this creates an interesting problem, because the method signatures of most of the functional interfaces do not declare to throw any exceptions. So, for instance, this is not possible:

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

做不到是因为write操作抛出了一个checked异常(即IOException)但是Consumer方法的签名没有声明它会抛出任何异常.所以,这个问题的唯一解决方案是创建更多的接口,一些声明异常,一些不声明(或者在语言级别为 异常透明度.同样,为了让事情不那么糟糕",专家组决定在这种情况下什么都不做.

It cannot be done because the write operation throws a checked exception (i.e. IOException) but the signature of the Consumer method does not declare it throws any exception at all. So, the only solution to this problem would have been to create even more interfaces, some declaring exceptions and some not (or come up with yet another mechanism at the language level for exception transparency. Again, to make things "less worse" the expert group decided to do nothing in this case.

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

In the words of Brian Goetz in the lambda mailing list:

[...] 是的,您必须提供自己出色的 SAM.但是之后lambda 转换适用于它们.

[...] Yes, you'd have to provide your own exceptional SAMs. But then lambda conversion would work fine with them.

EG 讨论了对此的额外语言和库支持问题,最后觉得这是一个糟糕的成本/收益权衡.

The EG discussed additional language and library support for this problem, and in the end felt that this was a bad cost/benefit tradeoff.

基于库的解决方案导致 SAM 类型激增 2 倍(例外与否),这与现有的组合爆炸相互作用很差用于原始专业化.

Library-based solutions cause a 2x explosion in SAM types (exceptional vs not), which interact badly with existing combinatorial explosions for primitive specialization.

可用的基于语言的解决方案是失败者复杂性/价值权衡.虽然有一些选择我们将继续探索解决方案——尽管显然不是8 个,也可能不是 9 个.

The available language-based solutions were losers from a complexity/value tradeoff. Though there are some alternative solutions we are going to continue to explore -- though clearly not for 8 and probably not for 9 either.

与此同时,您拥有了做您想做的事的工具.我明白了您希望我们为您提供最后一英里(其次,您的请求实际上是一个隐晦的请求,你为什么不给已经检查了异常"),但我认为当前的状态让你完成你的工作.[...]

In the meantime, you have the tools to do what you want. I get that you prefer we provide that last mile for you (and, secondarily, your request is really a thinly-veiled request for "why don't you just give up on checked exceptions already"), but I think the current state lets you get your job done. [...]

因此,由我们开发人员来制作更多的界面爆炸,以逐案处理这些问题:

So, it's up to us, the developers, to craft yet even more interface explosions to deal with these in a case-by-case basis:

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 中的值类型支持和具体化时,我们将能够摆脱(或至少不再需要使用)这些多个接口中的一些.

Probably, in the future when we get Support for Value Types in Java and Reification, we will be able to get rid of (or at least no longer need to use anymore) some of these multiple interfaces.

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

In summary, we can see that the expert group struggled with several design issues. The need, requirement or constraint to keep backward compatibility made things difficult, then we have other important conditions like the lack of value types, type erasure and checked exceptions. If Java had the first and lacked the other two the design of JDK 8 would probably have been different. So, we all must understand that these were difficult problems with lots of tradeoffs and the EG had to draw a line somewhere and make decisions.

这篇关于为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆