一、泛型的定义
泛型这个术语的意思是"适用于许多许多的类型"。它 是JDK 1.5的一项新特性,它的本质是 参数化类型 (Parameterized Type)的应用,也就是说 所操作的数据类型被指定为一个参数,在用到的时候再指定具体的类型。
1. 什么是参数化类型
参数化类型就是一个编译器可以自动定制作用于特定类型上的类 。举例:
List list = new ArrayList();
这是原生类型(未引入参数化类型时)的写法,list集合中可以存储不同类型的元素,如此便有了安全隐患,编译器不能保证你取值时的转型(拆箱)一定正确。
jdk1.5引入了参数化类型(泛型)之后,写法变为
List<类型(例如String)> list = new ArrayList();
这样的话,list中只能存储String类型的元素,编译器在编译时便会验证list中的元素是否全为String类型,否则编译错误。如此一来便不存在安全隐患,读取数据时也不需要自己进行拆箱,编译器会判断其元素类型为String。
简单的说就是, 原本集合中用来处理的通用类型为Object,而使用了参数化类型后,编译器会自动的将Object参数的类型修改为你传递给它的参数化类型,例如此例定义一个只接收和取出String的list容器。
二、 泛型的表达
一对尖括号,中间包含类型信息。常见的如T、E、K、V等形式的参数常用于表示泛型。如常用的集合
ArrayList<Food> foods = new ArrayList<Food>(); //此处的泛型类型是Food
ArrayList<T> list = new ArrayList<T>(); //此处的泛型类型是T,只是一个泛型标识,可以随意更改
三、 泛型的使用
泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。
- 泛型类
泛型作用于类上,叫泛型类。泛型类的定义规则:
public class Test<T>{//此处在实例化泛型类时,必须指定T的具体类
//key这个成员变量的类型为T, T的类型由外部指定
private T key;
public Test(T key) {
//泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){
return key;
}
}
传入的实参类型需与泛型的类型参数类型相同,即为String.
Test<String> testStr = new Test("Call me");
传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Test<Integer> testInteger = new Test(123);
System.out.println(testInteger.getKey())
System.out.println(testStr.getKey());
输出结果:
05-22 14:46:50.128 28166-28166/? I/System.out: Call me
05-22 14:46:50.129 28166-28166/? I/System.out: 123
我们在定义泛型类的时候,其实不是必要传入泛型类型实参。这里是为了更好的 体现泛型对类的约束 ,所以在成员变量中或构造方法中传入泛型类实参,从而在泛型类Test中,我们只能使用与泛型类参数一致的变量类型或方法类型。
- 泛型接口
泛型也可用于接口。与泛型类的使用和定义基本一致。看下面的例子:
public interface Kitchen<T> {//泛型定义接口
T cook();
}
class Cooker implemments Kitchen<String> {
String[] menu =new String[]{"Noodles", "Rice", "Dumplings"};
@Override
public String cook() {
int r = Random().nextInt(3);
return menu[r];
}
}
1.假设情景:客人随便来4份主食,通知后厨,厨师在厨房开始做饭。
Cooker cooker = new Cooker();
int i = 0;
while( i < 3){
Log.i("test","zhushi :"+ cooker.cook());
i ++;
}
结果:
05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Rice
05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Noodles
05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Noodles
05-22 18:16:48.677 22270-22270/com.example.papayawp.test I/test: zhushi :Noodles
- 1 泛型方法
到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型<T>,然后才可以用泛型<T>作为方法的返回值。就像下面这样:
public class TestMethods{
public <T> void readBook( T t){//定义泛型方法
Log.i( "test" , t.getClass().getName() );
}
public <T> T writeBook( T t){//标准定义泛型方法
Log.i( "test" , t.getClass().getName() );
}
public static void main(String[] args){
TestMethods test = new TestMethods();
test.readBook(" ");
test.readBook( 1 );
test.writeBook('c');
test.writeBook(test);
}
}
输出结果:
java.lang.String
java.lang.Integer
java.lang.Character
TestMethods
TestMethods并不是参数化的,尽管这个类和其内部的方法可以被同时参数化,但是在这个例子中,只有方法readBook()、writeBook() 拥有类型参数。这是由该方法的返回类型前面的类型参数列表指明的。注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型 。
为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。如我们可以像调用普通方法一样调用readBook(),而且就好像是readBook()被无限次地重载过。
下面介绍几种情况(引用其他人,例子很好),类似泛型方法却不是:
public class Test<T>{ // 这个是上面已经介绍过的泛型类
private T key;
public Test(T key) {
this.key = key;
}
问题1. 虽然在方法中使用了泛型,但是这并不是一个泛型方法。这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
问题2. 这也不是一个泛型方法,这就是一个普通的方法,只是使用了Test这个泛型类做形参而已。
public void look(Test test){
Log.d("test","key is "+ test.getKey());
}
问题3. 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class E", 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public T watch(Test<E> test){
...
}
问题4. 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class T"。对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。所以这也不是一个正确的泛型方法声明。
public void see(T t){
}
另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static 方法需要使用泛型能力,就必须使其成为泛型方法。
public class StaticTest<T> {
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show( T t ){..},此时编译器会提示错误信息:
* "StaticTest cannot be refrenced from static context"
*/
public static <T> void show( T t){//正确定义,必须在方法上泛型声明
}
}
3.2 泛型方法与可变参数
再看一个泛型方法和可变参数的例子:
public void printMsg( T... args){
for(T t : args){
Log.d("test", "t is "+ t);
}
}
printMsg("111",222,"aaaa",55.55);
泛型方法使得该方法能够独立与类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,就尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就只是用泛型方法,因为它可以使事情更清楚明白。
四、泛型通配符
public class Person{}
public class Student extends Person{}
我们知道Student是Person的一个子类。那么在使用Test作为形参的方法中,能否使用Test的实例传入呢?在逻辑上类似于Button和View是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Test这个泛型类继续看下面的例子:
public void readBook(Test<Person> test){
Log.d( "test" , "value is "+ test.getKey());
}
Test tStudent =new Test(new Student());
Test tPerson =new Test(new Student());
readBook(tPerson);
// readBook这个方法编译器会为我们报错:inferred type is Test<Student> but Test<Person> was Expected
通过提示信息我们可以看到Test<Student>不能被看作为Test<Person>的子类。由此可以看出: 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的 。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Test<Student>类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示 同时 是Test<Student>和Test<Person>父类的引用类型。由此类型通配符应运而生。
我们可以将上面的方法改一下:
public void readBook( Test<?> test ){
Log.d( "test", "value is " + test.getKey() );
}
再看一个例子:
public class ClassA<T> {
public T t;
public ClassA(T t) {
this.t = t;
}
public void read(ClassA<?> classA) {
System.out.println("test"+classA.t.getClass().getName());
}
}
调用过程:(Number是抽象类,Integer是它的实现子类)
ClassA<Integer> aa =new ClassA(123);
ClassA<Number> vv =new ClassA(333);//如果上面read方法中泛型参数类型是Integer,则vv.read(vv)会传入参数与方法参数类型不匹配错误
ClassA<Number> cc =new ClassA(555.0f);// Float也是Number的实现子类,在通配符的作用下,可以正常编译
aa.read(aa);
cc.read(cc);//输出结果是test java.lang.Integer test java.lang.Float
ClassA<String> ff =new ClassA("hello ,CiCi");
ff.read(ff) ; //输出结果是 test java.lang.String
类型通配符一般是使用 <?>代替具体的类型实参,注意,此处<?>是类型实参。通俗的讲就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
另外,如果想要只能接收Number类型的数据,不要String类型的数据。那么我们可以增加边界限定,
public class ClassA<T>{
public T t;
public ClassA(T t) {
this.t = t;
}
public void read(ClassA< ? extends Number> classA) {//让?继承Number类,此时方法只能接收Number类型的数据
System.out.println("test"+classA.t.getClass().getName());
}
}
测试:
ClassA<Integer> a =new ClassA(123);
a.read(a);
ClassA<Float> b =new ClassA(999.0f);
b.read(b);
ClassA<Double> c =new ClassA(199.99);
c.read(c);
ClassA<String> d =new ClassA("hello world");
d.read(d); //这一行代码编译器会提示错误,因为String类型并不是Number类型 的子类
五、总结
泛型的使用使我们的代码更加具有通用性,不会导致定义了一种类型之后其他的类型都无法使用该代码。通过泛型可以定义类型安全的数据结构(类型安全),而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到更高质量的代码(高性能),因为我们可以重用数据处理算法,而无须复制类型特定的代码(可重用)。
网友评论