在Java的泛型中有一些思考
我对背景不了解,但最近似乎有人热议Go语言和泛型。
Golang与泛型以及我
2017年3月9日下午10点33分
由于这段讲话有些情感化的煽动性,我想提前说明一下我的立场。最近我经常使用Go语言进行编程。以前我是使用Java语言的。我既喜欢Go语言,也喜欢Java语言。另外,我也喜欢Python语言。我认为它们都是很棒的编程语言。
我本身对于泛型并不熟悉,刚好正在重新学习泛型,所以原文对我来说非常有帮助。原文中@mattn所写的内容我大致认为是正确的,所以我同意他的观点,但是我觉得有几点解释被遗漏了,因此在本文中我想补充一些说明。
这篇文章提到了Java,但并不是在批评Java,同时也有意使用冗长的例子。
虽然有提到故意选择这样的例子,但在我看来,感觉有些偏向一方的说法。
有境界的类型参数
import java.util.List;
import java.util.Arrays;
public class Foo {
public static <T> T sum(List<T> list) {
int ret = 0;
for (T i : list) {
ret += i; // コンパイルエラー
}
return ret;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5,2,3,1,4);
System.out.println(sum<Integer>(list));
}
}
只有处理具有自由泛型的运算符的Java代码才不能编译。这样,就需要一个具有加法功能的接口。
当然可以使用接口来进行实现,但对于这个用途,我会选择不使用接口,而是按照以下方式进行实现。
import java.util.Arrays;
import java.util.List;
public class MyNumberUtil<T extends Number> {
public int sum(List<T> list) {
int ret = 0;
for (T i : list) {
ret += i.intValue();
}
return ret;
}
}
public class MyMain {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 2, 3, 1, 4);
MyNumberUtil<Integer> util = new MyNumberUtil<Integer>();
System.out.println(util.sum(list));
List<Double> doubleList = Arrays.asList(5.0, 2.0, 3.0, 1.0, 4.0);
MyNumberUtil<Double> doubleUtil = new MyNumberUtil<Double>();
System.out.println(doubleUtil.sum(doubleList));
}
}
如果没有指定类型变量的边界,Java 将将该类型变量视为 Object 类型。因此,在原文中,需要定义一个接口来表示。然而,如果我们知道 sum 函数的结果将返回 int 类型的值,我们可以定义 java.lang.Number 作为边界类型,以获取 int 类型的值。
ret += i.intValue();
当然,它也可以在Double类型下工作。
List<Double> doubleList = Arrays.asList(5.0, 2.0, 3.0, 1.0, 4.0);
MyNumberUtil<Double> doubleUtil = new MyNumberUtil<Double>();
System.out.println(doubleUtil.sum(doubleList));
使用Java的人通常会说:“讨厌Java的人经常写很多相同类型的代码(在这里是Double类型),太冗长了。”这点完全正确。对于这种人来说,使用lombok可以让他们变得更快乐。
仅凭接口无法检测到的事项。
在原始文章中,介绍了处理具有具有数字接口的多种类型混合的集合的Java和Go的实现示例。
list := []Numeric {
Float(5),
Int(2),
Int(3),
Float(1),
Int(4),
}
fmt.Println(sum(list))
我认为,在尝试实现这一点时,由于需要在运行时进行类型检查,因此无论是Java还是Go代码将变得相似。
然而,处理包含多个类型的集合是一种罕见的情况,通常情况下我们希望确保集合中不会混合多个类型。这就是Java和Go之间的重要区别所在。
在Go中,只能在运行时进行类型检查,但在Java中,可以使用泛型在编译时检测。
以下的例子是一段只在指定整数作为类型参数时才能运行的半成品代码,我将它作为一个可以通过类型参数检测错误的案例来介绍。稍后我会尝试让它能适用于其他类型,但如果有人知道如何实现,请告诉我 m(_ _)m
import java.util.Arrays;
import java.util.List;
public class MyMain {
public static void main(String[] args) {
List<MyNumber<Integer>> list = Arrays.asList(
new MyNumberImpl<Integer>(5),
new MyNumberImpl<Integer>(2),
new MyNumberImpl<Integer>(3),
new MyNumberImpl<Integer>(1),
new MyNumberImpl<Integer>(4)
);
MyNumberUtil<Integer> util = new MyNumberUtil<Integer>();
Integer r = util.sum(list).value();
System.out.println(r.intValue());
}
}
public interface MyNumber <T extends Number> {
MyNumber<T> add(MyNumber<T> i);
T value();
}
public class MyNumberImpl<T extends Number> implements MyNumber<T> {
private T x;
MyNumberImpl(T n) {
this.x = n;
}
@Override
@SuppressWarnings("unchecked")
public MyNumber<T> add(MyNumber<T> rhs) {
int value = this.x.intValue() + rhs.value().intValue();
return (MyNumber<T>) new MyNumberImpl<Number>(value);
}
@Override
public T value() {
return this.x;
}
}
public class MyNumberUtil<T extends Number> {
public MyNumber<T> sum(List<MyNumber<T>> list) {
MyNumber<T> ret = list.get(0);
for (int i=1; i<list.size(); i++) {
ret = ret.add(list.get(i));
}
return ret;
}
}
由于List<MyNumber>中指定了Integer作为类型参数,当混入了Double类型时,它会在编译时产生错误。我认为这是仅仅通过接口无法在编译时检测到的错误。
List<MyNumber<Integer>> list = Arrays.asList(
new MyNumberImpl<Integer>(5),
new MyNumberImpl<Integer>(2),
new MyNumberImpl<Double>(3.0),
new MyNumberImpl<Integer>(1),
new MyNumberImpl<Integer>(4)
);
总结。
对我来说,关于Go是否需要泛型的讨论并不重要,但我对泛型概念和类型系统的机制很感兴趣。
在《完全Java第2版》中,对于泛型类型作了以下的解释。
形式上的定义是,泛型是一种可以定义字段、方法参数和返回值之间的类型关系(哪种类型和哪种类型相同或不同)的语言特性。
如果对于Java和Go的泛型讨论感到疲劳,大家可以考虑使用Python作为替代。即便不去考虑这么复杂的问题,随意编写代码也能够相对顺利地运行。然而,代码能否让人满意又是另一个问题。
我认为语言功能也是语言的重要特征之一,然而我们不仅仅要讨论功能的存在与否,还要了解为什么会这样,并研究各个语言的文化和哲学,我相信一定会有有趣的发现。
参考资料
- http://qiita.com/yuroyoro/items/6bf33f3cd4bb35469e0b