目录
前言
接下来一段时间,本人会将近期所看到的学到的一些知识分享在CSDN上,欢迎各位大神前来交流、指教,首先会从JAVA开始,毕竟作为安卓开发工程师,JAVA是基础,后续也会更新一些安卓方面的内容。第一篇博客,是关于JAVA中的泛型。文章中的图表、代码均为原创,如需转载,请注明出处。
一、泛型是什么?
“泛型”的字面意思可以理解为“广泛的类型”、“很多很多的类型”。Java泛型是J2 SE1.5中引入的一个新特性,其本质是“参数化类型”,即将原本具体的类型作为一个可变的参数传递,从而达到“泛化类型”的目的。
二、为什么我们要使用泛型?
一般的类和方法中往往只能使用某种具体的类型,如果想要编写可以应用于多种类型的代码,可能需要重复写多份参数类型不同的代码。泛型的出现主要就是为了解决这种困境。下面是一个简单的例子。
现在我们想要实现一个简单的功能:传入一个数字,将其打印,现在我们希望无论是传入int类型还是double类型的数字,都能够正确将其打印,这时我们可能需要写两个方法,但两个方法的中逻辑相同,只是传入的参数类型和返回值类型不同:
public class MainClass {
public static void main(String[] args) {
printInt(1);//打印int型数字
printDouble(1.2);//打印double型数字
}
private static void printDouble(double num) {
System.out.println(num);
}
private static void printInt(int num) {
System.out.println(num);
}
}
上述的处理方式暂时能够实现打印int型和double型数字的需求,然而,如果以后还需要实现“打印long型数字”的需求,那我们又得把重复的代码再写一份,只是修改方法中传入和返回的参数类型,这种方式显然是效率低下、不易于维护的。有了泛型之后,我们就可以很轻松解决这个问题:
public class MainClass {
public static void main(String[] args) {
print(3L);//使用泛型方法打印long型数字
print(1);//使用泛型方法打印int型数字
print(1.2);//使用泛型方法打印double型数字
}
private static<T extends Number> void print(T num) {
System.out.println(num);
}
}
上面的代码中,我们使用了“泛型方法”,将之前形参中的int、double等类型泛化为T,同样实现了打印数字的功能,而且不用再写大量逻辑相同的代码。这就是泛型的其中一个好处。
泛型还能够给帮助我们避免类型转换错误的问题。List集合相信大家都用过,如果我们不使用泛型,那么任意类型都能够add进集合当中,并且IDE不会提示语法错误,如下:
import java.util.ArrayList;
import java.util.List;
public class MainClass {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add("1");//混进了一个String类型
for (int i = 0; i < list.size(); i++) {
int num = (int) list.get(i);
System.out.println(num * 2);
}
}
}
上面的例子中,我们先将集合中的对象强转为int,然后乘以2打印出来,但由于集合中混入了一个String类型数字,显然不能够强转为int,所以运行时就会报错。如果使用了泛型,就能够有效避免这一问题的出现,只要编译时期没有警告,那么运行时期就不会出现类型转换异常。
package com.example.libjava;
import java.util.ArrayList;
import java.util.List;
public class MainClass {
public static void main(String[] args) {
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add("1");//IDE会直接报错。因为使用了泛型,只允许add进去Integer类型的数据
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
System.out.println(num * 2);
}
}
}
上述代码在创建集合的时候就限定了存入元素的类型,什么类型的元素允许存入集合一目了然,增强了代码的可读性。
三、泛型的使用
3.1 泛型类
所谓泛型类就是把泛型定义在类上,使用该类的时候,才把类型明确下来,我们可以用如下的方式定义一个泛型类:
package com.example.libjava;
public class GenClass<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
上面就是一个简单的泛型类,泛型类型<T>需要声明在类名之后,此处的T也可以是AA,BB,DB等其他的字符串,语法上并没有太多的限制,但是建议遵循泛型类型的定义标准:通常使用大写字母T、E、K、V等来表示泛型参数,T:Type,E:Element,K:Key,V:value。泛型类上定义的泛型可以在类的普通方法中使用,但不能在静态域或静态方法中使用,这是因为非静态方法需要先有对象,进而在使用泛型的时候早已确定了具体类型。而静态方法不需要创建对象,代码的static部分是先执行的,然后才是构造函数,因此根本没有判断参数类型的约束条件,所以在编译时就报错了。虽然静态方法不能直接使用定义在类上的泛型,但可以采用泛型方法的定义方式,将泛型单独定义在方法上,从而获得使用泛型的能力。
3.2 泛型方法
如果我们只是想在某个方法上使用泛型,则可以只将泛型定义在某个方法上,类中的其他方法无法使用这个泛型,泛型方法中的泛型定义在返回值之前,下面是一个简单的泛型方法的定义:
public <V> void print(V content) {
System.out.println(content.toString());
}
需要说明的是,如果有需要,可以定义多个泛型,比如:
public <V,C> void print(V value,C content) {
System.out.println(content.toString());
System.out.println(value.toString());
}
3.3 泛型接口
和泛型类类似,泛型同样可以定义在接口上,并被其他类实现,实现类可以选择明确泛型类型,如下所示:
//泛型接口
public interface IGen<T> {
void print(T t);
}
//实现泛型接口的类
public class Print implements IGen<String> {
@Override
public void print(String s) {
System.out.println(s);
}
}
实现类也可以不明确泛型类型,而是继续使用泛型,等待其他子类来明确泛型类型:
package com.example.libjava;
public class Print<T> implements IGen<T> {
@Override
public void print(T s) {
System.out.println(s);
}
}
四、通配符与上下界
这一章开始之前,先看一张动物类的继承关系图,Animal类是他们的顶级父类。
然后再来看一段代码,代码的内容比较简单,打印两个泛型类的data字段。其中print方法的形参要求是一个GenClass<Man>类型的变量,在main方法中,new了两个对象,分别是GenClass<Man>类型和GenClass<YoungMan>类型,既然GenClass<Man>类型的变量能被print方法接受,那么YoungMan作为Man的子类,按说也应该能够被接受才合理,实际上IDE却会报错,提醒Required type和Provided type不一致,也就是说编译器认为GenClass<Man>类型和GenClass<YoungMan>是完全不相同的两个类,不具备任何继承关系,这一现象显然是不太符合面向对象编程思想的。
package com.example.libjava;
public class MainClass {
public static void main(String[] args) {
GenClass<Man> manGenClass = new GenClass<>();
GenClass<YoungMan> youngManGenClass = new GenClass<>();
print(manGenClass);
print(youngManGenClass);//报错
}
private static void print(GenClass<Man> humanGenClass) {
System.out.println(humanGenClass.getData());
}
}
为了解决上面的问题,通配符类型出现了。没有任何限制的通配符类型使用问号?来表示,意味着能够接受任意类型,我们通常称之为“无界通配符”。
有了通配符,我们就可以解决上面所说的问题,我们现在将print方法的形参改为GenClass<?>,意味着GenClass的泛型允许是任意类型,即使我们GenClass的泛型参数使用了Date和Object类型,IDE也不会再报错:
package com.example.libjava;
import java.util.Date;
public class MainClass {
public static void main(String[] args) {
GenClass<Animal> animalGenClass = new GenClass<>();
GenClass<Human> humanGenClass = new GenClass<>();
GenClass<Man> manGenClass = new GenClass<>();
GenClass<YoungMan> youngManGenClass = new GenClass<>();
GenClass<Object> objectGenClass = new GenClass<>();
GenClass<Date> dateGenClass = new GenClass<>();
print(manGenClass);//不报错
print(humanGenClass);//不报错
print(youngManGenClass);//不报错
print(animalGenClass);//不报错
print(objectGenClass);//不报错
print(dateGenClass);//不报错
}
private static void print(GenClass<?> humanGenClass) {
System.out.println(humanGenClass.getData());
}
}
但是,实际开发中,往往不需要如此之“泛”的类型,我们通常需要将类型控制在某些范围内,于是我们可以将?配合extends关键字和super关键字使用,从而控制上下界:
-
无界通配符 < ? >
-
上界通配符 < ? extends T>
-
下界通配符 < ? super T>
例如,< ? extends T>表示允许接受的泛型范围是T以及T的子类,其余类型都不能够被接受。如果T取Human,那么<? extends Human>允许的范围如下图红色部分所示:
再如,< ? super T>表示允许接受的泛型范围是T以及T的父类,其余类型都不能够被接受。如果T取Human,那么<? super Human>允许的范围如下图红色部分所示:
通过代码,也能很好地验证上述理论,对于print(GenClass<? extends Human> humanGenClass)方法,形参GenClass的泛型传入Animal、Object、Date,都超过了图中红色的范围,因此是不被允许的。
对于print2(GenClass<? super Human> humanGenClass)方法,形参GenClass的泛型传入Man、Date,也超过了图中红色的范围,也是不被允许的。
方法:
package com.example.libjava;
import java.util.Date;
public class MainClass {
public static void main(String[] args) {
GenClass<Animal> animalGenClass = new GenClass<>();
GenClass<Human> humanGenClass = new GenClass<>();
GenClass<Man> manGenClass = new GenClass<>();
GenClass<YoungMan> youngManGenClass = new GenClass<>();
GenClass<Object> objectGenClass = new GenClass<>();
GenClass<Date> dateGenClass = new GenClass<>();
print(manGenClass);//不报错
print(humanGenClass);//不报错
print(youngManGenClass);//不报错
print(animalGenClass);//报错
print(objectGenClass);//报错
print(dateGenClass);//报错
print2(manGenClass);//报错
print2(humanGenClass);//不报错
print2(youngManGenClass);//报错
print2(animalGenClass);//不报错
print2(objectGenClass);//不报错
print2(dateGenClass);//报错
}
private static void print(GenClass<? extends Human> humanGenClass) {
System.out.println(humanGenClass.getData());
}
private static void print2(GenClass<? super Human> humanGenClass) {
System.out.println(humanGenClass.getData());
}
}
五、泛型在虚拟机中是如何实现的?
JAVA中的泛型,更像是一种“伪泛型”,它只在程序源码中存在,在编译后的.class文件中,会先将泛型替换为原来的原生类型,然后加入强制转型,以这种方式来实现泛型。这也就意味着,一旦进入运行期,GenClass<Human>、GenClass<Man>、GenClass<OldMan>本质上都是一个类,此时的泛型仿佛被“擦除掉了”,他们都是原生类型GenClass。下图所示的代码中,我们尝试重载print方法,按说两个print方法的形参类型不同的话,是能够重载的,但是IDE出现了报错,报错信息直接翻译是“两个方法具有相同的擦除”,这也就意味着,两个形参GenClass<Man>和GenClass<Woman>在擦除后本质上类型是相同,所以才不允许重载,印证了前面所说的泛型擦除机制。
有了泛型擦除的理论基础,不如实际打开.class文件看一看,看是不是我们说的这样。MainClass源代码如下:
package com.example.libjava;
import java.util.ArrayList;
import java.util.List;
public class MainClass {
public static void main(String[] args) {
GenClass<String> stringGenClass=new GenClass<>();
GenClass<Human> humanGenClass=new GenClass<>();
GenClass<Man> manGenClass=new GenClass<>();
GenClass genClass=new GenClass();
GenClass<Object> genClass2=new GenClass<>();
}
}
接着我们打开Android studio中存放.class文件的路径,找到MainClass.class,看到的内容如下图所示。
查看.class文件内容,我们发现,之前在JAVA文件中尖括号内声明的泛型已经全部不见了,只剩下原生类,再次印证了上文中所说的泛型擦除机制。
总结
上面是我学习JAVA泛型后整理的部分内容,如有错误还请各位大神指出,邮箱hbutys@vip.qq.com