Java重载的知识中,在一个类内方法可以重载,Java编译器根据调用方法时传入的参数类型,参数个数来确定调用的是哪个方法。
// java
int add(int a, int b) {
return a + b;
}
float add(float a, float b) {
return a + b;
}
假设上述add
方法包含需要在设计时包含所有可相加的原始类型,那么就需要写多个功能相同的方法。那么是不是可以只写一个方法来涵盖所有的类型相加的功能呢?答案就是泛型。
泛型可以让类型(class/interface)在定义类时变成参数(parameter)。
泛型的优点:
编译期更强的类型检查。
Java编译器在编译时会对代码进行类型检查,如果代码违反类型安全规则,编译器会发出错误警告。这可以避免在运行时出现 ClassCastException。在编译期修改错误比在运行后修改出现的错误付出的代价要小。
消除类型转换。
由于泛型提供了编译时的类型检查,所以不需要在代码中进行显式的类型转换。
下面代码是不使用泛型的情况。
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 不使用泛型是,获取列表中item后还需要进行类型转换
在使用泛型之后。
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // 使用泛型后,获取item后,不需要显示的转换
代码重用:可以用一个类或者接口来操作不同类型的对象,这大大提高了代码的重用性。
提高程序可读性和稳定性:使用泛型可以使代码更易读,更稳定,更易于维护。
例如:
List<String> list = new ArrayList<String>();
这个通过泛型创建的String列表,再想添加其他类型,编译器就会报错。
文章开头的代码在使用泛型后可以修改成。
<T> T add(T a, T b) { // 修改为泛型方法
return a +b;
}
泛型类型是在类型上参数化的泛型类或接口。
下面是一个非泛型的具体类Box
。
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
Box
类中只定义了set
get
方法,参数Object表示可以向set方法方中传入任意的引用类型。Java编译器在编译这段代码时不会进行类型检查,因为set方法方中传入任意的引用类型,在使用过程中也可能造成意外的错误。
泛型类的定义方式格式:
public class ClassName<T1, T2, ..., Tn> { /* ... */ }
其中,T1, T2, …, Tn 是类型参数,它们在实例化类的时候会被具体的类型替换。
看下使用了泛型定义后的Box
类。
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
注意:类型参数T不能是原始数据类型。将原始类型传给类型参数,编译器会报错
// 在代码中创建Box实例,将int类型传入,代码编译时报错。
Box.java:15: error: unexpected type
Box box = new Box();
^
required: reference
found: int
Note: Box.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 error
正确的创建使用方式。
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(Integer.valueOf(13));
Integer someInteger = integerBox.get();
泛型中类型参数的命名惯例一般是单个大写字母表示。
下面是最常用的类型参数名:
使用泛型类创建实例时,需要指定类型参数的具体类型。
Box<Integer> integerBox = new Box<Integer>();
在JDK 7之后,创建泛型类实例,调用类的构造方式时,可以省去类型参数,直接写空的<>括号。
Box<Integer> integerBox = new Box<>();
上述的Box类中包含了单个的T类型参数。其实泛型类中的类型参数个数可以有多个,最典型的就是android中Pair类。
public class Pair<F, S> {
public final F first;
public final S second;
public Pair(F first, S second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(@Nullable Object o) {
if (!(o instanceof Pair)) {
return false;
}
Pair<?, ?> p = (Pair<?, ?>) o;
return Objects.equals(p.first, first) && Objects.equals(p.second, second);
}
@Override
public int hashCode() {
return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
}
@Override
public String toString() {
return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}";
}
public static <A, B> Pair <A, B> create(A a, B b) {
return new Pair<A, B>(a, b);
}
}
使用时,将类型参数 F,S 替换为具体类型。
Pair<String, Integer> p1 = new Pair<>("Even", 8);
Pair<String, String> p2 = new Pair<>("hello", "world");
已经具体化的类型也可以作为类型传给类型参数。
List<Box<Integer>> list = new ArrayList<>();
这行代码中,Box
是一个泛型类,创建的具体类型是Box
,将这个参数化后的Box类作为类型放入到List的声明中。
泛型类在用以创建实例时,类型参数位置可以不替换为任何类型,且<>括号也省去。
Box b = new Box();
这里的Box
是Box
的原始类型。之所以可以使用原始类型,因为泛型是在JDK 5之后引入的,为了后向兼容,保留了原始类型的使用。
以下两种程序,编译器会显示警告,但程序还是可以运行。
// 将泛型实例赋值给一个原始类型变量
Box<Integer> b = new Box<>();
Box b1 = b; // Box is a raw type. References to generic type Box should be parameterized
// 将原始类型实例赋值给一个泛型实例变量
MyBox b2 = new MyBox();
MyBox<Integer> b3 = b2;
这个警告显示原始类型绕过了泛型类型检查,从而将捕获不安全代码的操作推迟到运行时。因此,您应该避免使用原始类型。