Java泛型食用笔记(一) -- 基本介绍
JDK5 将泛型引入,这是 Java 走向类型安全的一大步,然而,在学习和使用泛型的过程中,几乎都会遇到令人沮丧的问题。本系列试图将 Java 的泛型解释清楚,帮助开发者在开发中正确的使用泛型。
1. 没有泛型的糟糕世界
引入泛型的原因不少,其中一个重要原因就是为了容器类的优雅实现。
在没有泛型前,当需要实现一个可以存储任何类型对象的 Hashtable 时,会定义如下接口:
public class Hashtable {
...
public Object put(Object key, Object value) {...}
public Object get(Object key) {...}
...
}
当你向 Hashtable 中插入对象时,比如一个 String 对象,Hashtable 会把类型信息抹去,退化成 Object 对象进行存储。此时只有你的脑袋中存有类型信息,当你需要从 Hashtable 获取元素时,必须对其进行强制类型转换才能获得你所需的 String 对象。
...
Hashtable h = new Hashtable();
h.put("string", "value");
String s = (String)h.get("string");
...
这样的代码直觉上就不太让人放心。每次强制类型转换,不仅增加冗余的代码,同时也是一次忽略编译器进行静态类型检查的过程,如果转换过程中出现错误,就会抛出 ClassCastException 异常,除非你能确保转换无误,否则你需要更过的代码来进行错误处理。在泛型出来之前,这几乎是无解题。
2. 泛型的引入
Java 泛型的核心就是告诉编译器想使用什么类型,然后编译器帮你处理一切细节。泛型也是一种参数,只不过参数传入的是类型。引入泛型可以让一些实现更优雅。
使用泛型重新定义 Hashtable 及其接口:
public class Hashtable<K, V> {
...
public V put(K key, V value) {...}
public V get(K key) {...}
...
}
其中 <K, V>
即为类型参数,其作用域为类定义的主体部分(除静态成员)。当使用泛型类时,你要将你所需使用的类型传入。
...
Hashtable<String, String> h = new Hashtable<>();
h.put("string", "value");
String s = h.get("string");
...
使用泛型后的 Hashtable 类更加简洁。我们来看下泛型带来了什么:
- 将类型信息告诉编译器
- 放入元素时编译器进行类型检查
- 取出元素自动转换类型
编译器在编译期间就会进行类型检查,防止在运行期间出现类型错误。
3. 泛型接口
泛型也可用于接口,比如需要写一个生成器:
interface Gernerator<T> {
T next();
}
class DrinkGernerator implements Gernerator<Drink> {
private Drink[] drinks = {new Water(){}, new Coke(){}, new Coffee(){}};
private Random seed = new Random();
@Override
public Drink next() {
return drinks[seed.nextInt(3)];
}
}
abstract class Drink {
public abstract String name();
}
class Water extends Drink {
@Override
public String name() {
return "Water";
}
}
class Coke extends Drink {
@Override
public String name() {
return "Coke";
}
}
class Coffee extends Drink {
@Override
public String name() {
return "Coffee";
}
}
看上去是不是特别眼熟,容器的迭代器 Iterator<E>
就是一个典型的生成器接口。
4. 泛型方法
之前据的所有例子都是作用与整个类的,泛型也可以仅仅应用在方法上,也就是接下来要介绍的泛型方法。原则上,能够使用泛型方法的时候就尽量避免使用泛型类,这会使你的代码看上去更加清楚。另外,如果 static 方法需要使用泛型,只能使用泛型方法。
泛型方法的使用方法就是将泛型参数置于返回值之前:
public class GernericMethod {
public static <T> void printClassName(T t) {
System.out.println(t.getClass().getName());
}
public static void main(String[] args) {
printClassName("string");
printClassName(1);
printClassName(2.1);
}
}
output:
java.lang.String
java.lang.Integer
java.lang.Double
看上去 printClassName
方法就像无限重载过,无论传入什么类型的参数,都可以顺利执行。这是因为泛型方法在使用时,编译器会进行参数推断,帮助我们找到具体的类型。
小结
泛型可以用于泛型类,泛型接口,泛型方法,并都有各自的使用场景。引入泛型后,可以避免很多蹩脚的类型转换等操作,让代码实现更为优雅,看上去一切都很美好。接下来我们将探讨 Java 泛型的实现原理及带来的问题,以帮助我们更好的理解和使用 Java 泛型