一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义类型。如果要编写可用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
泛型入门
Java的集合有个缺点:当我们把一个对象丢进集合里后,集合就会忘记者个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了 Object 类型。这是因为设计集合的程序员不知道我们要用集合来保存什么类型的对象,所以他们把集合设计成能保存任意类型的对象。这样做带来两个问题:
- 集合对元素类型没有任何限制,这样可能会引发一系列问题:例如,我们想创建一个只可以保存 Dog 对象的集合,但程序员也可以轻易地将 Cat 对象丢进去,这样会引发异常。
- 将对象丢进集合时,集合丢失了对象的状态信息,集合只知道它盛装的是 Object ,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换会增加程序的复杂度,而且可能引发 ClassCastException。
深入泛型
所谓泛型,就是允许在定义类、接口时指定类型形参,这个类型形参将在声明变量、创建对象时确定。
定义泛型接口、类
不仅 Java 的集合都定义成了泛型,用户自己也可以定义任意泛型的类、接口,只要在定义他们时用<>来指定类型参数即可。
- 定义泛型类
package com.chao.chapterFourteenGenerics;
public class OverClass<T> {
private T over;
public T getOver(){
return over;
}
public void setOver(T over){
this.over = over;
}
public static void main(String[] args) {
//实例化一个boolean类型的对象
OverClass<Boolean> over1 = new OverClass<Boolean>();
//实例化一个float类型的对象
OverClass<Float> over2 = new OverClass<Float>();
over1.setOver(true);
over2.setOver(2.0f);
Boolean b = over1.getOver();
Float f = over2.getOver();
System.out.println(b);
System.out.println(f);
}
}
对 class OverClass< T > 的理解:泛型类,也就是说这个类具体时什么类型是不确定的,也就是说 OverClass 可以是 String、可以是 Float、可以是 Boolean 也可以是自定义的 Apple 类型。他到底是什么类型要在创建这个类的对象的时候确定。如:
OverClass<String> str = new OverClass<String>();
这里,OverClass相当于String。
从泛型类派生子类
当创建了带有泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类来派生子类,但值得指出的是,当使用这些接口、父类时不能再包含类型形参,如下面代码就是错误的:
//类、接口中的类型形参:只有在定义类、接口时才可以使用类型形参,当使用类、接口时应为类型形参传入实际的类型(***)
public class A extends Apple<T>{}
可以改为如下代码:
public class A extends Apple<String>{}
方法中的形参,只有当定义方法时才可以使用数据形参,当调用方法时必须为这些数据形参传入实际的数据;与此类似的是:类、接口中的类型形参,只有在定义类、接口时才可以使用类型形参,当使用类、接口时应为类型形参传入实际的数据。
并不存在泛型类
前面提到我们可以把 ArrayList 类当作是 ArrayList 的子类,事实上 ArrayList 类也确实是一种特殊的 ArrayList 类,这个 ArrayList 对象是能添加 String 对象作为集合元素。但实际上,系统并没有为 ArrayList 生成新的 class 文件,而且也不会把 ArrayList 当作新类来处理。
public class Test {
public static void main(String[] args) {
List<String> list1 = new ArrayList<String>();
List<Double> list2 = new ArrayList<Double>();
//调用 getClass() 方法来比较 list1 和 list2 的类是否相等
System.out.println(list1.getClass() == list2.getClass());
}
}
//结果返回 true
上述代码输出的结果为 true ,因为不管泛型类型的实际类型参数是什么,它们在运行时总有同样的类。
因为类的静态变量和方法在所有实例间共享,所以在静态方法、静态初始化或者静态变量的声明或初始化中不允许使用类型形参。
类型通配符
我们知道 Integer 是 Number 的一个子类,同时我们也验证过 **Generic 与 Generic 实际上是相同的一种类型。**我们知道 Generic 不能看作是 Generic 的子类。我们需要一个在逻辑上可以表示同时是 Generic 和 Generic 父类的引用类型,因此类型通配符应运而生。
public void show(Genetic<?> obj){
Log.d("泛型测试");
}
类型通配符一般使用(?)代替具体的类型实参,**注意,此处(?)是类型实参,而不是类型形参。**此处的 (?) 和 Number、String、Integer 一样都是一种实际的类型。可以把(?)看作是所有类型的父类,是一种真实的类型。
泛型
设定类型通配符的上限
当直接使用 List<?> 这种形式时,即表明 List 集合可以是任何泛型 List 的父类。当我们不想 List<?> 是任何泛型 List 的父类,只想表示它是某一类泛型 List 的父类。
class Pet{
}
class Dog extends Pet{
}
public class Test2 {
public static void main(String[] args) {
List<Pet> pList = new ArrayList<Pet>();
pList.add(new Pet());
pList.add(new Pet());
//子类对象
pList.add(new Dog1());
}
public static void sys(List<? extends Pet> c){
for(int i = 0; i < c.size(); i++){
System.out.println(c.get(i));
}
}
}
在这里,List 不是 List 的父类,所以如果写成
public static void sys(List<Pet> c){
for(int i = 0; i < c.size(); i++){
System.out.println(c.get(i));
}
}
就会报错。List<? extends Pet> 可以表示所有 Pet 类型泛型的父类,所以写成这种形式是正确的。
List<? extends Pet> 是受限制通配符的一个例子,此处的 ? 代表一个未知类型,此处这个为止的类型总是 Pet 的一个子类(也可以是 Pet 类本身)。也可以把 Shape 当作这个通配符的上限。
设定类型形参的上限
Java 泛型不仅允许在使用通配符形参时使用类型上限,也可以在定义类型形参时设定上限。
public class Apple<T extends Number>{
T col;
public static void main(String[] args){
Apple<Integer> ai = new Apple<Integer>();
Apple<Double> ad = new Apple<Double>();
//下面代码将引起编译异常
//因为String类型传给T形参,但String不是Number的子类型
Apple<String> as = new Apple<String>();
}
}
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
泛型方法,就是在声明方法时,使用一个或多个类型形参。用法格式:
/*
* 1. public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2. 只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3. <T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
*/
public <T> T genericMethod(Class<T> tclass){
T instance = tclass.newInstance();
return instance;
}
将上面的方法和普通方法进行对比,发现泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号(,)隔开。
public class TestGenericMethod {
//声明一个泛型方法,该方法负责将一个Object数组的所有元素添加到一个Collection
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for(T o : a)
c.add(o);
}
public static void main(String[] args) {
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
//T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
//下面代码中的T代表String类型
fromArrayToCollection(sa, cs);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
//下面T代表Number类型
fromArrayToCollection(ia, cn);
fromArrayToCollection(fa, cn);
fromArrayToCollection(na, cn);
//下面代码中的T代表String类型,但na是一个Number数组,所以编译出错
//fromArrayToCollection(na, cs);
}
}
与接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面的 fromArrayToCollection() 方法,无须在调用该方法时传入 String、Object 等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值。它通常推断出最直接的类型参数。例如下面调用代码:fromArrayToCollection(sa, cs);
cs 是一个 Collection 类型,与方法定义时的 fromArrayToCollection(T[] a, Collection c) 进行比较,只比较泛型参数,不难发现 T 形参代表的实际类型是 String 类型。
为了让编译器准确推断出泛型方法中类型形参的类型,不要制造迷惑!!!
public class Test{
//声明一个泛型方法,该泛型方法中带一个T形参
static <T> void test(Collection<T> a, Collection<T> c){
for(T o : a)
c.add(o);
}
public static void main(String[] args){
List<Object> ao = new ArrayList<Object>();
List<String> as = new ArrayList<String>();
//编译错误
test(as, ao);
}
}
test() 方法中的两个形参 a、c 的类型都是 Collection,这要求调用该方法时两个集合实参中的泛型类型相同,否则编译器无法准确推断出泛型方法中类型形参的类型。
泛型方法和类型通配符的区别
public interface Collection<E>{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}
上面集合中的两个方法都采用了类型通配符的形式。
设定通配符的下限
假设自己实现一个工具方法:实现将 src 集合里的元素复制到 dist 集合中,因为 dist 集合中可以保存 src 集合里的所有元素,所以 dist 集合元素的类型应该是 src 集合元素类型的父类。
public static <T> void copy(Collection<T> dist, Collection<? extends T> src){
for(T ele : src)
dist.add(ele);
}