Java基础 -- 深入理解泛型
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
而泛型很好的解决了这个问题,这也是Java SE5的重大变化之一,下面将会深入介绍泛型。
一 泛型的概念
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
1、什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2、一个的例子
在前面博客中,我曾经介绍过Java持有对象(容器)。不过,有一个问题当时忽略了:在Java SE5之前的容器是允许向其中插入不正确类型。例如,考虑一个Apple对象的容器,使用最基本最可靠的容器ArrayList,在本例中Apple和Orange都被放置在了容器中,然后通过get()将他们取出,正常情况下,Java编译器会报告警告信息,因为这个示例没有使用泛型。在这里,使用Java SE5所特有的注解来抑制警告信息,注解以“@”符号开头,接受参数rawtypes。
import java.util.*; class Apple{ private static long counter; private final long id = counter++; public long id() { return id; } } class Orange{} public class ApplesAndOrangesWithoutGenerics { @SuppressWarnings("rawtypes") public static void main(String[] args) { ArrayList apples = new ArrayList(); for(int i=0;i<3;i++) { apples.add(new Apple()); } apples.add(new Orange()); for(int i=0;i<apples.size();i++) { System.out.println(((Apple)apples.get(i)).id()); } } }
输出结果:
0 1 2 Exception in thread "main" java.lang.ClassCastException: Orange cannot be cast to Apple at ApplesAndOrangesWithoutGenerics.main(ApplesAndOrangesWithoutGenerics.java:25)
Apple和Orange类是有区别的,他们除了都是Object之外没有任何共性。因为ArrayList保存的是Object,因此我们不仅可以通过ArrayList的add()方法将Apple对象放进这个容器,还可以添加Orange对象,而且在无论在编译期还是运行期都不会有问题。但是当使用ArrayList的get()方法取出我们认为是Apple的对象时,我们得到的只是Object引用,必须将其转换为Apple,因此需要将整个表达式括起来,在调用Apple的id()方法前,强制类型转换。否则,将会得到语法错误。在运行时,当尝试将orange对象转换为Apple时,就会抛出异常。为了解决这样的问题,泛型应运而生,通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。
下面还是这个例子,但是使用了泛型:
import java.util.ArrayList; public class ApplesAndOrangesWithGenerics { public static void main(String[] args) { ArrayList<Apple> apples = new ArrayList<Apple>(); for(int i=0;i<3;i++) { apples.add(new Apple()); } //apples.add(new Orange()); for(int i=0;i<apples.size();i++) { System.out.println(((Apple)apples.get(i)).id()); } } }
输出如下:
0 1 2
二 泛型的特性
1、擦除特性
泛型只在编译阶段有效。看下面的代码:
import java.util.*; public class Character { public static void main(String[] args) { List<String> stringArrayList = new ArrayList<String>(); List<Integer> integerArrayList = new ArrayList<Integer>(); Class classStringArrayList = stringArrayList.getClass(); Class classIntegerArrayList = integerArrayList.getClass(); if(classStringArrayList.equals(classIntegerArrayList)) { System.out.println("泛型测试:类型相同"); } } }
输出如下:
泛型测试:类型相同
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上可以看成是多个不同的类型,实际上是相同的基本类型(我们可以把这种基本类型假设为Object类型)。
Java泛型的这种特性,将会带来一个问题:
///java泛型存在的问题 class HasF{ public void f() { System.out.println("HasF.f()"); } } class Manipulator<T>{ private T obj; public Manipulator(T x) { obj = x; } //Error:cannot find symbol:method f() public void manipulate() { //这里是无法调用f()函数的 //obj.f(); } } public class Manipulation { public static void main(String[] args) { HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<HasF>(hf); manipulator.manipulate(); } }
由于有了擦除,Java编译器是无法将manipulate()方法在obj上调用f()这一需求映射到HasF拥有f()这一事实上。但是我们可以通过给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里重用了extends关键字,由于有了边界,下面的代码就可以编译了:
class Manipulator<T extends HasF>{ private T obj; public Manipulator(T x) { obj = x; } public void manipulate() { obj.f(); } }
边界<T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。如果情况确实如此,那么就可以安全在obj上调用f()了。
此外,我们还需要注意:
- 泛型的类型参数只能是类类型,不能是简单类型;
- 由于程序在运行时,会擦除泛型信息,因此任何在运行时需要知道确切类型信息(类对应的Class对象)的操作都无法工作。如下面的操作是非法的,编译时会出错。
if(manipulator instanceof Manipulator<HasF>){} //ERROR if(manipulator instanceof Manipulator<?>){} //可以 T[] array = new T[5]; //ERROR T 创建T的对象,需要知道类型T对应的Class对象 var = new T(); //ERROR T[] array = (T)new Object[5]; //Unchecked warning
2、创建类型实例
我们对创建一个new T()的尝试无法实现,这主要有两个原因:
- 泛型在运行时的擦除特性;
- 编译器无法验证T具有默认的构造器。
Java有多种解决方案,我们可以传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么就可以使用newInstance()来创建这个类型的新对象:
//使用内建的工厂对象 Class<T>是内建的工厂对象,可能存在一些问题,在编译器发现不了的,因此不推荐 class ClassAsFactory<T>{ T x; //创建一个kind类型对象实例 public ClassAsFactory(Class<T> kind) throws InstantiationException, IllegalAccessException{ x = kind.newInstance(); } } class Employee{} public class InstantiateGenericType { public static void main(String[] args) throws InstantiationException, IllegalAccessException { ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class); System.out.println("ClassAsFactory<Employee> succeeded"); try { //会失败,因为Integer没有任何默认的构造器,无法执行newInstance() ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); }catch(Exception e){ System.out.println("ClassAsFactory<Integer> failed"); } } }
输出如下:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
这可以编译,但是会因ClassAsFactory<Integer>而失败,因为Integer没有默认的构造器。因为这个错误不是在编译期捕获的,所以Sun对这种方式不推荐,他们推荐使用显示的工厂,并将限制其类型,使得智能接受实现了这个工厂的类:
///工厂模式 使用显式的工厂 推荐 interface FactoryI<T>{ T create(); } class Foo<T>{ private T x; //接受一个实现了工厂接口FactoryI的类 public <F extends FactoryI<T>> Foo(F factory){ x = factory.create(); } } //Integer工厂类 class IntegerFactory implements FactoryI<Integer>{ //生成一个Integer对象 @Override public Integer create() { // TODO Auto-generated method stub return new Integer(0); } } class Widge{ //内部工厂类 public static class Factory implements FactoryI<Widge>{ @Override public Widge create() { // TODO Auto-generated method stub return new Widge(); } } } public class FactoryConstraint { public static void main(String[] args){ new Foo<Integer>(new IntegerFactory()); new Foo<Widge>(new Widge.Factory()); } }
上面两种方式都传递了工厂对象,Class<T>碰巧是内建的工厂对象,而上面的方式创建了一个显式的工厂对象,但是却获得了编译期检测。
另外一种方式是模板设计模式:
package generic.factory; ///模板方法设计模式:create()是在子类中定义的、用来产生子类类型的对象 //模板类:用来创建T类型的对象 abstract class GenericWithCreate<T> { final T element; GenericWithCreate(){ element = create(); } //用来产生T类型的对象 abstract T create(); } //抽象类的实现类 用于创建Integer类型的对象 class Creator extends GenericWithCreate<Integer>{ Integer create() {return new Integer(0);} void f() { System.out.println(element.getClass().getSimpleName()); } } public class CreatorGeneric{ public static void main(String[] args) { Creator c = new Creator(); c.f(); } }
三 泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{ private 泛型标识 /*(成员变量类型)*/ var; ..... } }
一个最普通的泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ //key这个成员变量的类型为T,T的类型由外部指定 private T key; //泛型构造方法形参key的类型也为T,T的类型由外部指定 public Generic(T key) { this.key = key; } //泛型方法getKey的返回值类型为T,T的类型由外部指定 public T getKey(){ return key; } public static void main(String[] args) { //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型 //传入的实参类型需与泛型的类型参数类型相同,即为Integer. Generic<Integer> genericInteger = new Generic<Integer>(123456); //传入的实参类型需与泛型的类型参数类型相同,即为String. Generic<String> genericString = new Generic<String>("key_vlaue"); System.out.println("泛型测试:" + "key is " + genericInteger.getKey()); System.out.println("泛型测试:" + "key is " + genericString.getKey()); } }
输出结果如下:
泛型测试:key is 123456
泛型测试:key is key_vlaue
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
看一个例子:
Generic generic = new Generic("111111"); Generic generic1 = new Generic(4444); Generic generic2 = new Generic(55.55); Generic generic3 = new Generic(false); System.out.println("泛型测试:" + "key is " + generic.getKey()); System.out.println("泛型测试:" + "key is " + generic1.getKey()); System.out.println("泛型测试:" + "key is " + generic2.getKey()); System.out.println("泛型测试:" + "key is " + generic3.getKey());
输出如下:
泛型测试:key is 111111 泛型测试:key is 4444 泛型测试:key is 55.55 泛型测试:key is false
四 泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生成器中,这是一种专门负责创建对象的类。一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next()方法。
可以看一个例子:
//定义一个泛型接口 public interface Generator<T> { public T next(); }
当实现泛型接口的类,未传入泛型实参时:
/** * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" */ class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; } }
当实现泛型接口的类,传入泛型实参时:
/** * 传入泛型实参时: * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T> * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 */ public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } }
下面看一个具体的例子,CoffeeGenerator 是一个生成器类,实现Generator<Coffee>接口,它能够随机生成不同类型的Coffee对象:
import java.util.*; ///泛型接口,生成器 interface Generator<T>{ T next(); } class Coffee{ private static long counter = 0; private final long id = counter++; public String toString() { return getClass().getSimpleName() + " " + id; } } class Latte extends Coffee{} class Mocha extends Coffee{} class Cappuccino extends Coffee{} class Americano extends Coffee{} class Breve extends Coffee{} //Coffee生成器(实现了Generator泛型接口、以及Iterable接口->可以用来迭代遍历生成器每个元素) public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee>{ private Class[] types = {Latte.class,Mocha.class,Cappuccino.class,Americano.class,Breve.class}; private static Random rand = new Random(47); public CoffeeGenerator() {} //设置生成器大小 private int size = 0; public CoffeeGenerator(int sz) {size = sz;} //每次生成一个对象 @Override public Coffee next() { // TODO Auto-generated method stub try { return (Coffee)types[rand.nextInt(types.length)].newInstance(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } //实现Iterator接口(这里只是一个简单实现,很多细节没有处理),从而可以迭代遍历每个元素 class CoffeeIterator implements Iterator<Coffee>{ //保存剩余元素的个数 int count = size; @Override public boolean hasNext() { // TODO Auto-generated method stub return count > 0; } @Override public Coffee next() { // TODO Auto-generated method stub count--; return CoffeeGenerator.this.next(); } public void remove() { throw new UnsupportedOperationException(); } } //用于创建迭代器对象 @Override public Iterator<Coffee> iterator() { // TODO Auto-generated method stub return new CoffeeIterator(); } public static void main(String[] args) { CoffeeGenerator gen = new CoffeeGenerator(); for(int i=0;i<5;i++) System.out.println(gen.next()); //循环语句之所以可以使用,只是因为实现了Iterable接口 for(Coffee c:new CoffeeGenerator(5)) System.out.println(c); } }
输出如下:
Americano 0 Latte 1 Americano 2 Mocha 3 Mocha 4 Breve 5 Americano 6 Latte 7 Cappuccino 8 Cappuccino 9
参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个"末端哨兵"来判断何时停止,这正是第二个构造器的功能。
五 泛型通配符
我们知道Ingeter是Number的一个子类,同时在泛型特性中我们也验证过Generic<Ingeter>与Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>和Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Generic<T>
这个泛型类继续看下面的例子:
public void showKeyValue(Generic<Number> obj){ System.out.println("泛型测试:" + "key value is " + obj.getKey()); }
Generic<Integer> gInteger = new Generic<Integer>(123); Generic<Number> gNumber = new Generic<Number>(456); //The method showKeyValue(Generic<Number>) in the type Generic<T> is not applicable for the arguments (Generic<Integer>) //showKeyValue(gInteger); showKeyValue(gNumber);
通过提示信息我们可以看到Generic<Integer>不能被看作为Generic<Number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<Integer>类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<Integer>和Generic<Number>父类的引用类型。由此类型通配符应运而生。
我们可以将上面的方法改一下:
public void showKeyValue1(Generic<?> obj){ System.out.println("泛型测试:" + "key value is " + obj.getKey()); }
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
六 泛型方法
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
要定义泛型方法,只需将泛型参数列表置于返回值之前,就像下面那样:
/** * 泛型方法的基本介绍 * @param tClass 传入的泛型实参 * @return T 返回值为T类型 * 说明: * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 */ public <T> T genericMethod(Class<T> tClass)throws InstantiationException , IllegalAccessException{ T instance = tClass.newInstance(); return instance; }
Object obj = genericMethod(Class.forName("test"));
1、泛型方法的基本用法
光看上面的例子可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。
public class GenericTest { //这个类是个泛型类 public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。 //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。 //所以在这个方法中才可以继续使用 T 这个泛型。 public T getKey(){ return key; } /** * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E" * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。 public E setKey(E key){ this.key = keu } */ } //这不是一个泛型类 private static class GenericMethods{ //这是一个泛型方法 public <T> void f(T x) { System.out.println(x.getClass().getSimpleName()); } } /** * 这才是一个真正的泛型方法。 * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T * 这个T可以出现在这个泛型方法的任意位置. * 泛型的数量也可以为任意多个 */ public <T> T showKeyName(Generic<T> container){ System.out.println("container key :" + container.getKey()); //当然这个例子举的不太合适,只是为了说明泛型方法的特性。 T test = container.getKey(); return test; } //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已 public void showKeyValue1(Generic<Number> obj){ System.out.println("泛型测试:" + "key value is " + obj.getKey()); } //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符? //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类 public void showKeyValue2(Generic<?> obj){ System.out.println("泛型测试:" + "key value is " + obj.getKey()); } /** * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' " * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。 * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。 public <T> T showKeyName(Generic<E> container){ ... } */ /** * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' " * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。 * 所以这也不是一个正确的泛型方法声明。 public void showkey(T genericObj){ ... } */ public static void main(String[] args) { GenericTest.GenericMethods gm = new GenericTest.GenericMethods(); gm.f(""); gm.f(1); gm.f(1.0); gm.f(1.0F); gm.f('c'); gm.f(gm); } }
输出如下:
String
Integer
Double
Float
Character
GenericMethods
注意:当使用泛型类时,必须创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找到具体的类型,这称为类型参数推断(type argument inference)。
2、类型参数推断
当使用泛型类创建一个对象时,我们是需要指明参数类型的,就像下面程序这样:
Map<Person,List<? extends Pet>> petPeople = new HashMap<Person,List<? extends Pet>>(); //Map<Person,List<? extends Pet>> petPeople = new HashMap(); //虽然不会报错,但是会出现编译警告
上面这种情况时,编译器无法从泛型参数列表中的一个参数推断出另一个参数。但是,在泛型方法中,类型参数推断是可以工作的。例如,我们编写一个工具类,它包含各种各样的static方法,专门用来创建各种常用的容器对象:
///一个工具类,专门用来创建各种常用的容器对象 import java.util.*; public class New { public static <K,V> Map<K,V> map(){ return new HashMap<K,V>(); } public static <T> List<T> list(){ return new ArrayList<T>(); } public static <T> LinkedList<T> llist(){ return new LinkedList<T>(); } public static <T> Set<T> set(){ return new HashSet<T>(); } public static <T> Queue<T> queue(){ return new LinkedList<T>(); } }
main方法演示了如何使用这个工具类,类型参数推断避免了重复的泛型参数列表:
import java.util.*; class Person{} class Pet{} public class SimplePets { public static void main(String[] args) { Map<Person,List<? extends Pet>> petPeople = New.map(); } }
如果你将一个泛型方法调用的结果(例如New.map())作为参数,传递给另一个方法,这时编译器也会执行类型参数推断。下面的例子证明了这一点:
import java.util.*; public class LimitsOfInference { static void f(Map<Person,List<? extends Pet>> petPeople) { System.out.println(petPeople.getClass().getName()); } public static void main(String[] args) { f(New.map()); } }
输出:
java.util.HashMap
3、泛型方法与可变参数
泛型方法与可变参数可以很好的共存:
import java.util.*; public class GenericVarargs { public static <T> List<T> makeList(T...args){ List<T> result = new ArrayList<T>(); for(T item:args) { result.add(item); } return result; } public static void main(String[] args) { List<String> ls = makeList("A"); System.out.println(ls); ls = makeList("A","B","C"); System.out.println(ls); ls = makeList("ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("")); System.out.println(ls); } }
输出如下:
[A]
[A, B, C]
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
makeList()方法展示了与标准类库中java.util.Arrays.asList()方法相同的功能。
4、用于Generator的泛型方法
利用生成器,我们可以很方便的填充一个Collection,而泛型化这种操作是具有实际意义的:
///斐波那契数列 public class Fibonacci implements Generator<Integer>{ private int count = 0; @Override public Integer next() { // TODO Auto-generated method stub return fib(count++); } private int fib(int n) { if(n<2) return 1; return fib(n-2)+fib(n-1); } public static void main(String[] args) { Fibonacci gen = new Fibonacci(); for(int i=0;i<18;i++) { System.out.println(gen.next()); } } }
import java.util.*; public class Generators { public static <T> Collection<T> fill(Collection<T> coll,Generator<T> gen,int n){ for(int i=0;i<n;i++) coll.add(gen.next()); return coll; } public static void main(String[] args) { Collection<Coffee> coffee = fill(new ArrayList<Coffee>(),new CoffeeGenerator(),4); for(Coffee c:coffee) System.out.println(c); Collection<Integer> fnumbers = fill(new ArrayList<Integer>(),new Fibonacci(),12); for(int i:fnumbers) System.out.print(i + ", "); } }
输出如下:
Americano 0 Latte 1 Americano 2 Mocha 3 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
5、一个通用的Generator
下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器。为了减少类型声明,它提供一个泛型方法,用以生成BasicGenerator:
public class BasicGenerator<T> implements Generator<T> { private Class<T> type; //构造函数 public BasicGenerator(Class<T> type) { this.type = type; } @Override public T next() { // TODO Auto-generated method stub try { return type.newInstance(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } //静态方法:用于创建一个生成器对象 public static <T> Generator<T> create(Class<T> type){ return new BasicGenerator<T>(type); } }
这个类提供了一个基本实现,用以生成某个类的对象。这个类它必须具备默认构造器(无参数构造器);
要创建这样的BasicGenerator对象,只需调用create()方法,并传入想要生成的类型。泛型化的create()方法允许执行BasicGenerator.create(MyType.class),而不必执行麻烦的new BasicGenerator<MyType>(MyType.class)。
例如下面是一个具有默认构造器的简单的类:
public class CountedObject { private static long counter = 0; private final long id = counter++; public long id() { return id; } public String toString(){ return "CountedObject " + id; } }
CountedObject类能够记录下它创建了多少个CountedObject实例,并通过toString()方法告诉我们其编号。
使用BasicGenrator,可以容易的为CountedObject创建一个Generator:
public class BasicGeneratorDemo { public static void main(String[] args) { //创建一个生成器对象 Generator<CountedObject> gen = BasicGenerator.create(CountedObject.class); for(int i=0;i<10;i++) { System.out.println(gen.next()); } } }
输出如下:
CountedObject 0 CountedObject 1 CountedObject 2 CountedObject 3 CountedObject 4 CountedObject 5 CountedObject 6 CountedObject 7 CountedObject 8 CountedObject 9
可以看到,使用泛型方法可以创建Generator对象,大大减小了我们要编写的代码。Java泛型要传入Class对象,以便也可以在create()方法中用它进行类型参数推断。
6、一个Set实用工具
作为泛型方法的另一个实例,我们看看如何用Set来表达数学中的关系式。通过泛型方法,我们很方便的做到这一点,而且可以应用于多种类型:
import java.util.*; ///一个Set实用工具 public class Sets { //合并两个Set集合 public static <T> Set<T> union(Set<T> a,Set<T> b){ Set<T> result = new HashSet<T>(a); result.addAll(b); return result; } //提取两个Set共有的部分 public static <T> Set<T> intersection(Set<T> a,Set<T> b) { Set<T> result = new HashSet<T>(a); result.retainAll(b); return result; } //Subtract subset from superset public static <T> Set<T> difference(Set<T> superset,Set<T> subset) { Set<T> result = new HashSet<T>(superset); result.removeAll(subset); return result; } //获取除了交集以外的元素 public static <T> Set<T> complements(Set<T> a,Set<T> b) { return difference(union(a,b),intersection(a,b)); } }
在前面三个方法中,都将第一个参数Set复制了一份,将Set中的所有引用都存入一个新的HashSet对象中,因此,我们并未直接修改参数中的Set,返回的是一个全新的Set对象。
这四个方法表达了如下的数学集合操作:
- union():返回一个Set,它将两个Set合并在一起;
- intersection():返回的Set包含两个参数共有的部分;
- difference():从superset中移除subset包含的元素;
- complement():返回的Set包含除了交集之外的所有元素。
下面提供一个enum,它包含各种水彩画的颜色,我们将用它来演示以上这些方法的功能和效果:
public enum Watercolors { ZINC,LEMON_YELLOW,MEDIUM_YELLOW,DEEP_YELLOW,ORANGE, BRILLIANT_RED,CRIMSON,MAGENTA,ROSE_MADDER,VIOLET, CERULEAN_BLUE_HUE,PHTHALO_BLUE,ULTRAMARINE, COBALT_BLUE_HUE,PERMANENT_GREEN,VIRIDIAN_HUE, SAP_GREEN,YELLOW_OCHRE,BURNT_SIENNA,RAW_UMBER, BURNT_UMBER,PAYNES_GRAY,IVORY_BLACK }
为了方便起见,下面的示例以static的方式引入了Watercolors。这个示例使用了EnumSet,用来从enum直接创建Set。在这里,我们向static方法EnumSet.range()传入某个范围第一个元素和最后一个元素,然后它返回一个Set,其中包含该范围内的所有元素:
import java.util.*; import static Watercolors.*; import static Sets.*; public class watercolorSets { public static void print(String msg) { System.out.println(msg); } public static void main(String[] args) { Set<Watercolors> set1 = EnumSet.range(BRILLIANT_RED,VIRIDIAN_HUE); Set<Watercolors> set2 = EnumSet.range(CERULEAN_BLUE_HUE,BURNT_UMBER); print("set1:" + set1); print("set2:" + set2); print("union(set1,set2):" + union(set1,set2)); print("intersection(set1,set2):" + intersection(set1,set2)); print("difference(set1,set2):" + difference(set1,set2)); print("complements(set1,set2):" + complements(set1,set2)); } }
输出如下:
set1:[BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE]
set2:[CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBER]
union(set1,set2):[VIOLET, CERULEAN_BLUE_HUE, ROSE_MADDER, SAP_GREEN, BURNT_SIENNA, ULTRAMARINE, RAW_UMBER, MAGENTA, PERMANENT_GREEN, BURNT_UMBER, CRIMSON, VIRIDIAN_HUE, YELLOW_OCHRE, COBALT_BLUE_HUE, BRILLIANT_RED, PHTHALO_BLUE]
intersection(set1,set2):[CERULEAN_BLUE_HUE, VIRIDIAN_HUE, COBALT_BLUE_HUE, ULTRAMARINE, PERMANENT_GREEN, PHTHALO_BLUE]
difference(set1,set2):[CRIMSON, VIOLET, ROSE_MADDER, BRILLIANT_RED, MAGENTA]
complements(set1,set2):[VIOLET, ROSE_MADDER, SAP_GREEN, BURNT_SIENNA, RAW_UMBER, MAGENTA, BURNT_UMBER, CRIMSON, YELLOW_OCHRE, BRILLIANT_RED]
7、静态方法与泛型
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法。
对于一个静态方法而言:静态方法无法访问泛型类的类型参数;所以,如果静态方法需要使用泛型能力,就必须使其成为泛型方法。
public class StaticGenerator<T> { /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void show(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){ } }
8、泛型上下边界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
-
为泛型添加上边界,即传入的类型实参必须是指定类型的子类型
public void showKeyValue1(Generic<? extends Number> obj){ System.out.println("泛型测试:" + "key value is " + obj.getKey()); }
Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Float> generic3 = new Generic<Float>(2.4f); Generic<Double> generic4 = new Generic<Double>(2.56); //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类 //showKeyValue1(generic1); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4);
如果我们把泛型类的定义也改一下:
public class Generic<T extends Number>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } }
//这一行代码也会报错,因为String不是Number的子类 Generic<String> generic1 = new Generic<String>("11111");
再来一个泛型方法的例子:
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加 //public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound" public <T extends Number> T showKeyName(Generic<T> container){ System.out.println("container key :" + container.getKey()); T test = container.getKey(); return test; }
通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。
七 泛型数组
在泛型的特性中,我们已经了解到:程序在运行时,会擦除泛型信息,因此任何在运行时需要知道确切类型信息的操作都无法工作。因此,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
T[] array = new T[SIZE]; Generic<Integer>[] array = new Generic<Integer>[SIZE]; List<String> [] array = new List<String>[SIZE];
成功创建泛型数组的唯一方式:创建一个被擦除类型的新数组,然后对其转型:
T[] array = (T[])new Object[SIZE]; Generic<Integer>[] array = (Generic<Integer>[])new Generic[SZIE]; List<String>[] array = (List<String>[])new List[SIZE];
但是有一点我们需要注意:第一个array的实际运行时类型是Object[],后面两个也是类似的。我们来看一个具体的示例:
///简单的泛型数组包装器 public class GenericArray<T>{ private T[] array; @SuppressWarnings("unchecked") public GenericArray(int sz){ //成功创建泛型数组的唯一方式:创建一个被擦除类型的新数组,然后对其转型: array = (T[]) new Object[sz]; } public void put(int index,T item) { array[index] = item; } public T get(int index) { return array[index]; } public T[] rep() { return array; } public static void main(String[] args) { GenericArray<Integer> gai = new GenericArray<Integer>(10); //异常 这主要是因为gai.rep()的实际运行时类型是Object[] //Integer[] ia = gai.rep(); Object[] oa = gai.rep(); System.out.println(oa.getClass().getSimpleName()); } }
rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试着将结果作为Integer[]引用来捕获,就会得到ClassCastException,这还是因为实际运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转为类型T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某潜在的错误检测。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。让我们看看这是如何作用于GenericArray.java示例的:
///简单的泛型数组包装器 public class GenericArray2<T>{ private Object[] array; public GenericArray2(int sz){ // array = new Object[sz]; } public void put(int index,T item) { array[index] = item; } @SuppressWarnings("unchecked") public T get(int index) { return (T)array[index]; } @SuppressWarnings("unchecked") public T[] rep() { //unchecked cast return (T[])array; } public static void main(String[] args) { GenericArray2<Integer> gai = new GenericArray2<Integer>(10); for(int i=0;i<10;i++) gai.put(i, i); for(int i=0;i<10;i++) System.out.print(gai.get(i) + " "); System.out.println(); try { Integer[] ia = gai.rep(); }catch(Exception e) { System.out.println(e); } } }
输出如下:
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
上面这种方式,如果你调用rep(),尝试将Object[]转换为T[],依然是不正确的,会在编译期产生警告,在运行期产生异常。因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]。
但是有一种方式可以让数组在运行时类型是确切类型T[],就是传递一个类型标记,具体来看下面这段代码:
import java.lang.reflect.Array; ///创建泛型数组:推荐方式 public class GenericArrayWithTypeToken<T> { private T[] array; @SuppressWarnings("unchecked") public GenericArrayWithTypeToken(Class<T> type,int sz) { array = (T[])Array.newInstance(type, sz); } public void put(int index,T item) { array[index] = item; } public T get(int index) { return array[index]; } public T[] rep(){ return array; } public static void main(String[] args) { GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class,10); Integer[] ia = gai.rep(); System.out.println(ia.getClass().getSimpleName()); } }
输出:
Integer[]
通过反射机制中的Array我们可以创建需要的实际类型的数组。就像在main()中看到的那样,该数组的运行时类型是确切类型T[]。
List<String>[] lsa = new List<String>[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK
参考文章
[1]Java编程思想
[2]java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一(部分转载)
[3]java 泛型详解
[4]Java中的泛型方法
[5]java泛型详解