java.util.Arrays
中的新方法没有为所有基元类型进行重载。我注意到的方法有:
目前,这些新方法仅处理int
、long
和double
基元类型。
int
、long
和double
可能是最广泛使用的基元类型,因此如果他们必须限制API,选择这三个是有道理的,但是他们为什么要限制API呢?java.util.Arrays
中的新方法没有为所有基元类型进行重载。我注意到的方法有:
目前,这些新方法仅处理int
、long
和double
基元类型。
int
、long
和double
可能是最广泛使用的基元类型,因此如果他们必须限制API,选择这三个是有道理的,但是他们为什么要限制API呢?为了回答这些问题,而不仅仅是这个特定的场景,我认为我们都想知道...
T1
,T2
,T3
,...,T16
),但在JDK 8中,我们拥有一组不同的功能接口,具有不同名称和不同的方法名称,并且其抽象方法表示众所周知的function arities的子集(即零元,一元,二元,三元等)。然后我们有处理原始类型的爆炸案例,甚至还有其他导致更多功能接口爆炸的情况。
因此,在某种程度上,这两种语言都遭受了某种形式的接口污染(或在C#中称为委托污染)。唯一的区别是,在C#中它们都有相同的名称。不幸的是,在Java中,由于类型擦除,Function<T1,T2>
和Function<T1,T2,T3>
或Function<T1,T2,T3,...Tn>
之间没有区别,因此显然我们不能简单地以相同的方式命名它们,并且我们必须为所有可能的函数组合类型想出创意名称。有关此问题的更多参考,请参阅Brian Goetz的How we got the generics we have。
请不要认为专家小组没有努力解决这个问题。用Brian Goetz的话说,他在lambda邮件列表中说:
[...] 以函数类型为例,让我们来看一下。在Devoxx提供的lambda草案中有函数类型。我坚持要删除它们,这使我不受欢迎。但是,我反对函数类型并不是因为我不喜欢函数类型——我喜欢函数类型——而是因为函数类型与Java类型系统的现有方面(即类型擦除)相冲突。被擦除的函数类型是两个世界中最糟糕的。所以我们从设计中删除了它。但我不愿意说“Java永远不会有函数类型”(尽管我认识到Java可能永远不会有函数类型)。我认为,为了获得函数类型,我们必须首先处理擦除。这可能是可能的,也可能不可能。但在具有具体结构类型的世界中,函数类型开始变得更加合理 [...]
这种方法的优点是我们可以定义自己的接口类型,其方法接受任意数量的参数,并且我们可以根据需要使用它们来创建lambda表达式和方法引用。换句话说,我们有权力污染世界,增加更多新的函数接口。此外,我们甚至可以为JDK早期版本或我们自己API的早期版本中定义的SAM类型创建lambda表达式。因此,现在我们有权将Runnable
和Callable
用作函数接口。Function0
、Function1
、Function2
、...、FunctionN
这样的接口。也许,我唯一能提出反对意见的理由是,他们希望最大化在早期版本的API中为接口定义lambda表达式的可能性,正如前面提到的那样。
因此,很明显类型擦除是其中一个驱动力。但如果你是那些想知道为什么我们还需要所有这些具有类似名称和方法签名的附加功能接口,并且唯一的区别是使用原始类型的人之一,那么让我提醒您,在Java中,我们缺乏像C#语言中那样的值类型。这意味着我们在泛型类中使用的泛型类型只能是引用类型,而不是原始类型。
换句话说,我们不能这样做:
List<int> numbers = asList(1,2,3,4,5);
List<Integer> numbers = asList(1,2,3,4,5);
我们可以看到这对专家组来说是一个艰难的决定。我认为很少有人会认为这是优美的,但大多数人可能会认为这是必要的。
如果您想进一步了解此主题,您可以阅读由John Rose、Brian Goetz和Guy Steele撰写的价值类型的状态。
还有第三个推动力可能会使情况更糟,那就是Java支持两种异常类型:已检查异常和未检查异常。编译器要求我们处理或明确声明已检查异常,但对于未检查异常则不需要任何声明。所以,这就创造了一个有趣的问题,因为大多数功能接口的方法签名都不声明抛出任何异常。因此,例如以下操作是不可能的:
Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error
由于write
操作抛出了已检查的异常(即IOException
),但是Consumer
方法的签名并没有声明它会抛出任何异常,因此无法实现。因此,解决这个问题的唯一方法就是创建更多的接口,有些声明异常,有些则不声明(或在语言层面上想出另一种机制来实现异常透明性)。为了让事情变得“不那么糟糕”,专家组决定在这种情况下不采取任何措施。
用Brian Goetz在lambda邮件列表中的话说:
"[...] 是的,您必须提供自己的异常SAM。但是,使用它们进行lambda转换将运行良好。因此,由我们开发人员来制定更多的接口爆炸,以针对每种情况进行处理。
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中值类型的支持和具体化时,我们将能够摆脱(或至少不再需要使用)一些这些多个接口。
总之,我们可以看到专家组在几个设计问题上遇到了困难。需要、要求或约束保持向后兼容性使事情变得困难,然后我们还有其他重要条件,如缺乏值类型、类型擦除和已检查异常。如果Java具有第一个并且缺少其他两个,则JDK 8的设计可能会有所不同。因此,我们都必须理解这些是具有很多权衡的困难问题,EG必须在某个地方划线并做出决策。
int[]
、long[]
和double[]
,已经有流操作了。因此,其他原始类型也应该有相应的操作。实际上,我觉得这非常有趣——Java最好笑了!哈哈。 - The Coordinator
java.util.function
中没有{Boolean,Byte,Char,Float,Short}UnaryOperator
接口”这个问题不一样吗?如果想要额外的重载工作而不需要对操作符输出进行拆箱和空检查,就需要这些接口。 - Mike Samuel