泛型概念
泛型,可以说是参数化类型,是将类型由原来的具体的类型参数化类似于方法中的变量参数,此时类型也定义为参数形式,然后在使用/调用的时候传入具体的类型。泛型的本质是为了参数化类型,也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类,接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
引入列子
List arrayList = new ArrayList();
arrayList.add("ssc"); //String类型
arrayList.add(123); //Integer类型
for(int i = 0;i < arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item=" + item);
}
这个时候,肯定会报错,ArrayList可以存放任意类型,例子中添加一个String类型,又添加一个Integer类型,再使用时都用String类型使用,程序会崩溃,此时为了解决这个问题,就出现了泛型。
还是这个例子,下面引入泛型
List<String> arrayList = new ArrayList<String>();
arrayList.add("ds");
arrayList.add(121); //程序编译报错
特性
泛型只在编译时使用
List<String> StringArrayList = new ArrayList<String>();
List<Integer> IntegerArrayList = new ArrayList<Integer>();
//getClass()方法的作用
//获得了Person这个(类)Class,进而通过返回的Class对象获取Person的相关信息,比如:获取Person的构造方法,方法,属性有哪些等等信息。
Class classString = StringArrayList.getClass();
Class classInteger = IntegerArrayList.getClass();
if(classString.equals(classInteger)){
Log.d("泛型测试","类型相同");
}
此时会正常输出。
上面的例子说明,在编译之后程序会采用去泛型化的措施。也就是说java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检验和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
泛型的使用
泛型有三种使用方式:泛型类,泛型接口,泛型方法
泛型类
泛型类用于类的定义中,被称为泛型类
#此处T可以随便写为任意标识、常见的如T、E、K、V等形式的参数常用于表示泛型
#在实例化泛型类时,必须指定T的具体类型
//一个普通的泛型类
public class Generic<T>{
//定义T类型的变量
private T key;
//有参构造
public Generic(T key){
this.key = key;
}
//得到T类型的key
public T getKey(){
return key;
}
}
#泛型的参数类型只能是类类型(包括自定义类型),不能是简单类型
#传入的实参类型需要与泛型的类型参数类型相同
Generic<Integer> genericInteger = new Generic<Integer>(123);//此时实参必须有数,因为有参构造为泛型,此时指定的类型为Integr
Generic<String> genericString = new Generic<String>("dsc");//此时为String
注意:
1.并不是定义的泛型类,就一定要传入泛型类型实参,如果传入的不是泛型类型的实参,在泛型类中使用泛型的方法或者成员变量定义的类型可以为任何的类型。
2.泛型的类型参数只能是类类型,不能是简单类型
泛型接口
定义一个泛型接口,与泛型类类似,只是把class变为interface
public interface Generator<T>{
public T next();
}
泛型接口需要注意的地方
当实现泛型接口的类,未传入泛型实参时:
未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。即:class FruitGeneric<T> implements Generator<T>{}
如果不声明泛型,如:class FruitGeneric implements Generator<T>
编译器会报错:"Unkonwn class"
class FruitGeneric<T> implements Generator<T>{
@Override
public T next(){
return null;
}
}
当实现泛型接口的类,传入泛型实参时:
传入泛型实参时,定义一个生产器实现这个接口,在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要变为传入的类型。
public class FruitGeneric implements Generator<String>{
private String[] fruits = new String[]{"apple","banana","pear"};
@Override
public String next(){
Rand rand = new Rand();
return fruits[rand.nextInt(3)];
}
}
泛型通配符
我们指定Integer是number的一个子类,Generic<Integer>和Generic<Number>实际是相同的一种基本类型。但是在逻辑上类似于Generic<Integer>和Generic<Number>不能看成具有父子关系的泛型类型。
public void showKeyValue(Generic<Number> obj){
Log.d("泛型测试","key value is" + obj.getKey());
}
Generic<Integer> genericInteger = new Generic<Integer>(123);
Generic<Number> genericString = new Generic<Number>(345);
showKeyValue(genericInteger);
此时会提示编译报错
我们需要一个在逻辑上可以表示同时是Generic<Integer>和Generic<Number>父类的引用类型。所以此时类型通配符应运而生
public void showKeyValue(Generic<?> obj){
Log.d("泛型测试","key value id" + obj.getKey());
}
类型通配符一般使用的是"?" 代替具体的类型实参,而不是类型形参。此处的?和String、Integer、Number一样都是一种实际的类型,可以把?看成所以类的夫类。
可以解决当具体类型不确定的时候,这个通配符就是?;当操作类型不需要使用类型的具体功能时,可以使用Object类的功能。那么可以用?通配符来表未知类型。
通配符的上界
现在举另一个例子,我们知道List<Cat>并不是List<Animal>的子类型,那就需要去寻找替他解决的办法, 使AnimalTrianer.act()方法变得更为通用(既可以接受List<Animal>类型,也可以接受List<Cat>等参数)。在java里解决办法就是使用通配符“?”,具体到AnimalTrianer,就是将方法改为act(List<? extends Animal> list),当中“?”就是通配符,而“? extends Animal”则表示通配符“?”的上界为Animal,换句话说就是,“? extends Animal”可以代表Animal或其子类,可代表不了Animal的父类(如Object),因为通配符的上界是Animal。如下,为改进之后的AnimalTrianer
public class AnimalTrainer {
public void act(List<? extends Animal> list) {
for (Animal animal : list) {
animal.eat();
}
}
}
再来测试一下,如下,发现可以通过编译了:
public class TestAnimal {
public static void main(String[] args) {
AnimalTrainer animalTrainer = new AnimalTrainer();
//Test 1
List<Animal> animalList = new ArrayList<>();
animalList.add(new Cat("cat1"));
animalList.add(new Bird("bird1"));
animalTrainer.act(animalList); //可以通过编译
//Test 2
List<Cat> catList = new ArrayList<>();
catList.add(new Cat("cat2"));
catList.add(new Cat("cat3"));
animalTrainer.act(catList); //也可以通过编译
}
}
经过上述分析,可以知道List<Animal>和List<Cat>都是List<? extends Animal>的子类型,类似有List<Bird>,List<Magpie>也是List<? extends Animal>的子类型。
现总结如下,对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List<E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))
- G<? extends Y> 是 G<? extends X>的子类型(如List<? extends Cat> 是 List<? extends Animal>的子类型)。
- G<X> 是 G<? extends X>的子类型(如List<Animal> 是 List<? extends Animal>的子类型)
- G<?> 与 G<? extends Object>等同,如List<?> 与List<? extends Objext>等同。
学到这里,可能会遇到一些疑惑的地方,或者说事理解不透的地方,先观察如下两段代码片段,判断一下其是否可行??
public void testAdd(List<? extends Animal> list){
//....其他逻辑
list.add(new Animal("animal"));
list.add(new Bird("bird"));
list.add(new Cat("cat"));
}
List<? extends Animal> list = new ArrayList<>();
list.add(new Animal("animal"));
list.add(new Bird("bird"));
list.add(new Cat("cat"));
先分析如下:因为“? extends Animal”可代表Animal或其子类(Bird,Cat),那上面的操作应该是可行的。事实上是”不行“,即无法通过编译。为什么呢??
在解释之前,再来重新强调一下已经知道的规则:在List<Aimal> list里只能添加Animal类对象及其子类对象(如Cat和Bird对象),在List<Bird>里只能添加Bird类和其子类对象(如Magpie),可不能添加Animal对象(不是Bird的子类),类似的在List<Cat>和List<Magpie>里只能添加Cat和Bird对象(或其子类对象,不过这没有列出)。现在再回头看一下testAdd()方法,我们知道List<Animal>、List<Cat等都是List<? extends Animal>的子类型。先假设传入的参数为为List<Animal>,则第一段代码的三个“add”操作都是可行的;可如果是List<Bird>呢??则只有第二个“add”可以执行;再假设传入的是List<Tiger>(Tiger是想象出来的,可认为是Cat的子类),则三个“add”操作都不能执行。
现在反过来说,给testAdd传入不同的参数,三个“add”操作都可能引发类型不兼容问题,而传入的参数是未知的,所以java为了保护其类型一致,禁止向List<? extends Animal>添加任意对象,不过却可以添加null,即list.add(null)是可行的。有了上面谈到的基础,再来理解第二段代码就不难了,因为List<? extends Animal>的类型“? extends Animal”无法确定,可以是Animal,Bird或者Cat等,所以为了保护其类型的一致性,也是不能往list添加任意对象的,不过却可以添加null。
先总结如下:不能往List<? extends Animal> 添加任意对象,除了null。
另外提醒大家注意的一点是,在List<? extends Animal> 可以是Animal类对象或Bird对象等(只是某一类对象),反过来说,在List<? extends Animal> list里的都是Animal对象,即Bird也是Animal对象,Cat也是Animal对象(用java的语言来说就是子类可以指向父类,父类却不能指向子类),那么在Animal里的所有方法都是可以调用的,如下:
for (Animal animal : list) { animal.eat(); }
通配符的下界
既然有了通配符的上界,自然有着通配符的下界。可以如此定义通配符的下界 List<? super Bird>,其中”Bird“就是通配符的下界。注意:不能同时声明泛型通配符申明上界和下界。
在谈注意细节之前,我们先看一下通配符的使用规则——对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List<E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))
- G<? super X> 是 G<? super Y>的子类型(如List<? super Animal> 是 List<? super Bird>的子类型)。
- G<X> 是 G<? super X>的子类型(如List<Animal> 是 List<? super Animal>的子类型)
现在再来看如下代码,判断其是否符合逻辑:
public void testAdd(List<? super Bird> list){
list.add(new Bird("bird"));
list.add(new Magpie("magpie"));
}
List<? super Bird> list = new ArrayList<>();
list.add(new Bird("bird"));
list.add(new Magpie("magpie"));
list.add(new Animal("animal"));
看第一段代码,其分析如下,因为”? super Bird”代表了Bird或其父类,而Magpie是Bird的子类,所以上诉代码不可通过编译。而事实上是”行“,为什么呢?2?
在解疑之前,再来强调一个知识点,子类可以指向父类,即Bird也是Animal对象。现在考虑传入到testAdd()的所有可能的参数,可以是List<Bird>,List<Animal>,或者List<Objext>等等,发现这些参数的类型是Bird或其父类,那我们可以这样看,把bird、magpie看成Bird对象,也可以将bird、magpie看成Animal对象,类似的可看成Object对象,最后发现这些添加到List<? supe Bird> list里的对象都是同一类对象(如本文刚开篇提到的Test 1),因此testAdd方法是符合逻辑,可以通过编译的。:
现在再来看一下第二段代码对于,第二、三行代码的解释和上文一样,至于最后一行“list.add(new Animal("animal"))”是无法通过编译的,为什么的??为了保护类型的一致性,因为“? super Bird”可以是Animal,也可以是Object或其他Bird的父类,因无法确定其类型,也就不能往List<? super Bird>添加Bird的任意父类对象。
既然无法确定其父类对象,那该如何遍历List<? super Bird> ? 因为Object是所有类的根类,所以可以用Object来遍历。如下,不过貌似其意义不大。
for (Object object : list) {//...}
那“? super BoundingType”可以应用在什么地方呢??“? super BoundingType”应用相对广泛,只不过是混合着用。下面举个简单的例子。先假设有以下两个Student和CollegeStudent,当中CollegeStudent继承Student,如下:
public class Student implements Comparable<Student>{
private int id;
public Student(int id) {
this.id = id;
}
@Override
public int compareTo(Student o) {
return (id > o.id) ? 1 : ((id < o.id) ? -1 : 0);
}
}
public class CollegeStudent extends Student{
public CollegeStudent(int id) {
super(id);
}
}
先需要根据他们的id对他们进行排序(注意此处是对数组对象进行排序),设计方法如下,(n指数组元素的个数):
public static <T extends Comparable<? super T>>
void selectionSort(T[] a,int n)
先理解此方法含义,首先<T extends Comparable<T>>规定了数组中对象必须实现Comparable接口,Comparable<? Super T>表示如果父类实现Comparable接口,其自身可不实现,如CollegeStudent。先假设有一个CollegeStudent的数组,如下:
CollegeStudent[] stu = new CollegeStudent[]{
new CollegeStudent(3),new CollegeStudent(2),
new CollegeStudent(5),new CollegeStudent(4)};
执行方法 selectionSort(stu,4)是完全可以通过的。可如果定义的selectionSort方法如下:
public static <T extends Comparable<T>>
void selectionSort(T[] a,int n)
则方法selectionSort(stu,4)不能执行,因为CollegeStudent没有实现Comparable<CollegeStudent>接口。换句话就是“? super T”使selectionSort方法变得更为通用了。
无界通配符
知道了通配符的上界和下界,其实也等同于知道了无界通配符,不加任何修饰即可,单独一个“?”。如List<?>,“?”可以代表任意类型,“任意”也就是未知类型。无界通配符通常会用在下面两种情况:
1、当方法是使用原始的Object类型作为参数时,如下:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + "");
System.out.println();
}
可以选择改为如下实现:
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + "");
System.out.println();
}
这样就可以兼容更多的输出,而不单纯是List<Object>,如下:
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
在定义的方法体的业务逻辑与泛型类型无关,如List.size,List.cleat。实际上,最常用的就是Class<?>,因为Class<T>并没有依赖于T。
最后提醒一下的就是,List<Object>与List<?>并不等同,List<Object>是List<?>的子类。还有不能往List<?> list里添加任意对象,除了null。
泛型方法
泛型方法,就是在调用方法的时候指明泛型的具体类型
1.tclass传入的泛型实参
2.T 返回值类型为T
3.public与返回值中间<T>非常重要,可以理解为声明此方法为泛型方法
4.只有声明<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法不是泛型方法
5.<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T
6.与泛型类的定义一样,此处T可以随便写为任意标识,常见T、V、K
public <T> T genericMethod(Class<T> tclass) throws InstantiationException,IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
Object obj = genericMethod(Class.forName(""));
泛型方法的基本用法
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
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
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){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","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) {
}
}
类中的泛型方法
当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
泛型方法与可变参数
再看一个泛型方法和可变参数的例子:
public <T> void printMsg( T... args){
for(T t : args){
Log.d("泛型测试","t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);
静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
泛型方法总结
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
泛型上下边界(可略过)
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
为泛型添加上边界,即传入的类型实参必须是指定类型的子类型
public void showKeyValue1(Generic<? extends Number> obj){
Log.d("泛型测试","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;
}
通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。
关于泛型数组要提一下
看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
List<String>[] ls = new ArrayList[10];
下面使用Sun的一篇文档的一个例子来说明这个问题:
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
文章部分摘自:@VieLei