在java 1.5之前,一般的类和方法,只能使用具体的类型(基础类型或自定义类型),如果要编写可以适应多种类型的代码,就只能使用多态这种泛化机制,然后通过强制类型转换来实现参数的“任意化”。但这种转换要求实际参数类型可以预知。且对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。
泛型的类型参数只能是类类型(包括自定义类),不能是基本数据类型(可以使其包装类)。
泛型的参数类型可以使用extends语句,例如
<T extends superclass>
。习惯上称为“有界类型”。- 泛型的参数类型还可以是通配符类型。例如
Class<?> classType =
Class.forName("java.lang.String");
1.泛型类
在Java 5之前,为了让类有通用性,往往将参数类型、返回类型设置为Object类型,当获取这些返回类型来使用时候,必须将其“强制”转换为原有的类型或者接口,然后才可以调用对象上的方法。例如:
class Gen {
public Object ob; // 定义一个通用类型成员
public Gen(Object ob) {
this.ob = ob;
}
public void getTyep() {
System.out.println("实际类型是: " + ob.getClass().getName());
}
}
public class GenDemo {
public static void main(String[] args) {
// Integer版本
Gen intOb = new Gen(new Integer(88));
intOb.getTyep();
System.out.println("value= " + (Integer) intOb.ob);//需要类型转换
System.out.println("---------------------------------");
// String版本
Gen strOb = new Gen("Hello Gen!");
strOb.getTyep();
System.out.println("value= " + (String) strOb.ob);//需要类型转换
}
}
强制类型转换很麻烦,还要事先知道各个Object具体类型是什么,才能做出正确转换。否则,要是转换的类型不对,就会出现运行时错误。而使用泛型就可以暂时先不指定类型,稍后再决定具体使用什么类型。
实现泛型版本
class Gen2<T> {
public T ob; // 定义泛型成员变量
public Gen2(T ob) {
this.ob = ob;
}
public void getType() {
System.out.println("T的实际类型是: " + ob.getClass().getName());
}
}
public class GenDemo2 {
public static void main(String[] args) {
// Integer版本
Gen2<Integer> intOb = new Gen2<Integer>(88);
intOb.getType();
System.out.println("value= " + intOb.ob);
System.out.println("----------------------------------");
// String版本
Gen2<String> strOb = new Gen2<String>("Hello Gen!");
strOb.getType();
System.out.println("value= " + strOb.ob);
}
}
元组
一次方法调用返回多个对象,是编程中经常用到的功能,但是return语句只能返回一个对象。通常的解决办法是创建一个对象来持有想要返回的多个对象,通过返回一个这种对象来获取我们需要的多个对象。有了泛型就能够简化这种操作,编写出通用的代码,同时在编译器确保类型安全。
元组(tuple)是将一组对象直接打包存储于一个单一对象中。这个对象允许读取其中的对象,不允许向其中放入新对象。这个概念也称为数据传送对象或信使。
元组可以具有任意长度,元组中的对象可以是任意不同的类型。例如:
//2维元组,持有两个对象
class TwoTuple<A,B> {
public final A a;
public final B b;
public TwoTuple(A a,B b) {
this.a = a;
this.b = b;
}
}
public class TupleTest {
//返回这个对象
public TwoTuple<String,Integer> f() {
return new TwoTuple<String,Integer>("hi",10);
}
}
注:对象a,b声明成final不声明成private不违反java编程的安全原则吗?
- 声明成private类型,a,b就只能在类内部使用,只有添加get,set方法才能通过对象来调用
- 声明成final的元素,当他们第一次被初始化以后就不能被修改,同样保证了编程的安全性。
可以通过继承机制实现类型更长的元组,例如:
//3维元组,持有三个对象
class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C c;
public ThreeTuple(A a,B b,C c) {
super(a,b);
this.c = c;
}
}
2.泛型方法
要定义泛型方法,只需将泛型参数列表置于返回值之前。是否拥有泛型方法,与其所在的类是否是泛型没有关系,泛型方法使得该方法能够独立于类而产生变化。
基本原则:如果使用泛型方法可以取代整个类泛型化,就应该使用泛型方法。
如:
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f(1); //输出:java.lang.Integer
gm.f("a"); //输出:java.lang.String
gm.f(gm); //输出:GenericMethods
}
}
注意:
- 使用泛型方法时,不必指明参数类型,编译器会自己找出具体的类型。这称为类型参数推断(type argument inference)。泛型方法除了定义不同,调用就像普通方法一样。
- 如果f()传入基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。
- 对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
类型推断只对赋值操作有效,如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。这时必须显示指明泛型方法的类型。如:
public class GenericMethods {
public <T> List<T> g() {
return new ArrayList<T>();
}
public void h(List<String> x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
List<String> list = gm.g(); //类型推断
//gm.h(gm.g()); //报错,不能由h中参数类型String,推断出g()中类型
gm.h(gm.<String>g()); //显示指明类型,输出:java.util.ArrayList
}
}
可变参数与泛型方法
可变参数:适用于参数个数不确定,类型确定的情况,java把可变参数当做数组处理。注意:可变参数必须位于最后一项。当可变参数个数多余一个时,必将有一个不是最后一项,所以只支持有一个可变参数。
可变参数的特点:
只能出现在参数列表的最后;
…位于变量类型和变量名之间,前后有无空格都可以;
- 调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中一数组的形式访问可变参数。
泛型方法与可变参数列表能够很好的共存:
public class GenericVarargs {
public static <T> void print(T ...array) {
for(T item : array) {
System.out.print(item + " ");
}
System.out.println();
}
public static void main(String[] args) {
print("A","B","C"); //输出:A B C
print(1,2,3,4); //输出:1 2 3 4
}
}
3.类型擦除与补偿
使用泛型时,java会在编译时检查类型的安全性,但会在运行时擦除所有这些信息。
import java.util.ArrayList;
import java.util.Arrays;
public class ErasedType {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);//输出:true
System.out.println(Arrays.toString(c1.getTypeParameters()));//输出:[E]
}
}
ArrayList<String>
和ArrayList<Integet>
在运行时事实上是相同的类型,这两种形式都被擦除成他们的原生类型,即ArrayList。
class.getTypeParameters()
将返回一个TypeVariable对象数组。
在泛型方法内部,无法获得任何有关泛型参数的类型信息。
如以下代码:
public class ErasedType2<T> {
public static void f(Object obj) {
if(obj instanceof T) { } //错误
T var = new T(); //错误
T[] array = new T[10]; //错误
}
}
虽然偶尔可以绕过这些问题来编程,但有时必须通过引入类型标签来对擦除进行补偿。这需要显式的传递类型的Class对象。
如代码:
public class TypeOffset<T> {
Class<T> obj;
public TypeOffset(Class<T> obj) {
this.obj = obj;
}
public boolean f(Object arg) {
return obj.isInstance(arg);
}
public static void main(String[] args) {
TypeOffset<Cat> to = new TypeOffset<Cat>(Cat.class);
System.out.println(to.f(new Cat())); //true
System.out.println(to.f(new Animal())); //false
}
}
class Animal { }
class Cat extends Animal { }
用动态的isInstance()方法,代替instanceof,判断是否是Cat类型的对象。
4.限制泛型
要限制泛型类class Generics<T>
中类型T为集合接口类型,只需要这么做:class Generics<T extends Collection>
,这样类中的泛型T只能是Collection接口的实现类,传入非Collection接口编译会出错。
实际上class Generics<T>
的限定类型相当于Object,这和“Object泛型”实质是一样的。
注意:<T extends Collection>
这里的限定使用关键字extends,后面可以是类也可以是接口。
限制为集合接口的类型:
public class CollectionGen<T extends Collection> {
private T x;
public CollectionGen(T x) {
this.x = x;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}
实例化:
public class CollectionGenDemo {
public static void main(String args[]) {
CollectionGen<ArrayList> listFoo = null;
listFoo = new CollectionGen<ArrayList>(new ArrayList());
System.out.println("实例化成功!");
}
}
当前看到的这个写法是可以编译通过,并运行成功。可是注释掉的两行加上就出错了,因为<T extends Collection>
这么定义类型的时候,就限定了构造此类实例的时候T是确定的一个类型,这个类型实现了Collection接口,但是实现 Collection接口的类很多很多,如果针对每一种都要写出具体的子类类型,那也太麻烦了,干脆还不如用Object通用一下。泛型针对这种情况还有更好的解决方案,那就是“通配符泛型”。
多接口限制
虽然Java泛型简单的用 extends 统一的表示了原有的 extends 和 implements 的概念,但仍要遵循应用的体系,Java 只能继承一个类,但可以实现多个接口,所以你的某个类型需要用 extends 限定,且有多种类型的时候,只能存在一个是类,并且类写在第一位,接口列在后面,也就是:
<T extends SomeClass & interface1 & interface2 & interface3>
这里的例子仅演示了泛型方法的类型限定,对于泛型类中类型参数的限制用完全一样的规则,只是加在类声明的头部,如:
public class Demo<T extends Comparable & Serializable> {
// T类型就可以用Comparable声明的方法和Seriablizable所拥有的特性了
}
5.通配符泛型
为了解决类型被限制死了不能动态根据实例来确定的缺点,引入了“通配符泛型”,使用通配泛型格式为
import java.util.*;
public class CovariantGenerics {
public static void main(String[] args) {
List<Apple> flist = new ArrayList<Apple>();
flist.add(new Apple());
//flist.add(new Orange()); //不能编译,
List<?> slist = flist;
System.out.println(slist.get(0).getClass().getName()); //输出:com.generics.Apple ,说明实际存入的为Apple类型
//Apple ap = slist.get(0); //但取出后为Object实例,不能赋给ap
}
}
class Fruit { }
class Apple extends Fruit { }
class Orange extends Fruit { }
注意:
- 如果只指定了
<?>
,而没有extends,则默认为<? extends Object>
。 - 通配符泛型不单可以向下限制,如
<? extends Collection>
,还可以向上限制,如<? super Double>
,表示类型只能接受Double及其上层父类类型,如Number、Object类型的实例。
例如:
import java.util.*;
public class CovariantGenerics {
public static void main(String[] args) {
List<Apple> flist = new ArrayList<Apple>();
flist.add(new Apple());
List<? extends Fruit> slist = flist;
//slist.add(new Apple()); //任何类型都不能存入
System.out.println(slist.get(0).getClass().getName()); //输出:com.generics.Apple ,说明实际存入的为Apple类型
Fruit ap = slist.get(0); //但取出后为Fruit实例
List<? super Apple> tlist = flist;
tlist.add(new RedApple()); //Apple及其子类能够存入
Object ob = tlist.get(0); //但取出后为Object
}
}
class Fruit { }
class Apple extends Fruit { }
class RedApple extends Apple { }
<? extends Fruit>
,通配符告诉编译器我们正在处理Fruit的子类型,但它不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,就不允许往里面加入任何这种类型的数据。另一方面,不论它是什么类型,它总是类型Fruit的子类型,当我们在读取数据时,能确保返回的数据是一个Fruit类型的实例
相应的对于<? super Apple>
,编译器知道我们正在处理Apple的父类型的数据,当我们加入一个Apple或其子类型的数据时,能够保证类型安全。另一方面,由于不知道我们处理的究竟是那种父类型,但一定能够保证是Object或其子类型,所以当读取数据时返回Object类型
参考:
- think in java
- 百度百科