Java泛型总结#
泛型是什么##
从本质上讲,泛型就是参数化类型。泛型十分重要,使用该特性可以创建类、接口以及方法,并且可以使用类型参数来指定类型。没有泛型以前,对于不确定的类型常常使用Object类,因为Object类是其他任何类的超类,可以创建任何类型的对象。但是缺点也十分明显,不能以类型安全的形式工作。泛型提供了以前没有的类型安全性,简化处理过程,不需要像使用Object类时需要类型转换,泛型提供自动的隐式类型转换。
一个简单的泛型实例##
创建Gen泛型类,类型参数为T。T可以是任何引用类型,使用时,可以传递任何引用类型。T在此处相当于就是一个占位符,其可以是任意引用类型。
public class Gen <T> {
T ob;
Gen(T ob){
this.ob = ob;
}
T getOb(){
return ob;
}
//显示类型参数的类型
void showType(){
System.out.println("T's Type is: " + ob.getClass().getName());
}
}
使用泛型,用具体的类型替换占位符T。
public class GenDemo {
public static void main(String[] args) {
Gen<String> strGen = new Gen<>("string type");
strGen.showType();
Gen<Integer> intGen = new Gen<>(100);
intGen.showType();
}
}
执行结果:
T's Type is: java.lang.String
T's Type is: java.lang.Integer
Process finished with exit code 0
可以看出,T的类型就是在使用泛型时传递的类型参数的类型。
Gen<String> strGen = new Gen<>("string type");
上面的语句将表示T的类型为String,此语句也可以声明为下面的形式,但是上面的形式更加简洁,从jdk 1.7开始可以去掉构造器中的类型参数,泛型提供了类型推断功能,因此可以省略,这也被称为泛型的菱形语法。
Gen<String> strGen = new Gen<String>("string type");
泛型只适用引用类型###
当声明泛型时,传递的参数必须是引用类型,使用简单类型是非法的。比如下面的声明:
Gen<int> genInt = new Gen<int>(100);//非法的
基于不同类型参数的泛型类型是不同的###
Gen<String> strGen = new Gen<>("string type");
Gen<Integer> intGen = new Gen<>(100);
上面的两行代码声明的泛型类型是不同的
strGen == intGen
上面的代码会出现编译错误。
泛型类型提升类型安全性的原理
我们先来看一个不使用泛型的例子。
public class Nor {
Object obj;
Nor(Object obj){
this.obj=obj;
}
Object getObj(){
return obj;
}
}
首先编译器不知道任何关于构造器传入的类型信息,这是一件坏事。其二,对于数据需要进行强制类型转换。
public class NorDemo {
public static void main(String[] args) {
Nor intNor = new Nor(11);
int a = (Integer) intNor.getObj();
String str = (String) intNor.getObj();
}
}
上面的代码可以看出,inNor.getObj()返回类型是Integer类型,但是却赋值给了String型,但是编译时能通过,因为编译器不知道Object的类型,但是很遗憾,运行时肯定会有类型转换的问题。
运行上面的代码抛出ClassCastException异常
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at simpledemo.NorDemo.main(NorDemo.java:11)
但是使用泛型就不会发生以上情况,泛型显示的声明了类型参数,这让开发人员和编译器都知道参数的类型。
带有两个类型参数的泛型类
public class TwoGen <T,V>{
T obj1;
V obj2;
TwoGen(T obj1, V obj2){
this.obj1 = obj1;
this.obj2=obj2;
}
T getObj1(){
return obj1;
}
V getObj2(){
return obj2;
}
}
和一个类型参数的泛型类差不多,菱形框里的参数可以有多个,如果类型参数总是一样,那就没有必要声明多个。比如下面的代码,声明一个类型参数就够了。
TwoGen<String,String> t = new TwoGen<>("aaa", "bbb");
有界类型##
从前面的例子可以看出,类型参数可以是任意的,但是往往一件事没有约束并不是好的。如果现在我们想写一个加法器,让任意的两个数字相加,这些数字可以是整型、浮点型的,如果使用泛型的方式,根据前面的经验,可以这样编码
public class Caculator <T> {
T a;
T b;
T getSum(T a, T b){
return a+b;//错误
}
}
那么问题来了,因为T可以任意类型的,而加运算只针对与数字和字符串连接,编译器并不知道T的类型,所以就更不知道这个+号是用来干嘛的了。
为了处理这种情况java提供了类型边界,我们先来看使用类型边界的实现:
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(3,4);
System.out.println(a);
Caculator<Float> floatCaculator = new Caculator<>();
double b = floatCaculator.getSum(9.0f,10.45f);
System.out.println(b);
Caculator<Double> doubleCaculator = new Caculator<>();
double c = doubleCaculator.getSum(1.0,2.4);
System.out.println(c);
}
}
结果:
7.0
19.449999809265137
3.4
Process finished with exit code 0
分析:
下面的代码说明类型参数为Number的子类,也就是说将T的范围限制在Number和Number的子类中
Caculator <T extends Number>
所以才可以使用a.doubleValue()方法,因为T继承自Number。还有一个好处就是可以防止传入不能处理的类型,比如String,因为String并不是Number子类,编译会报错。
当然也可以扩展一个或多个接口,使用&连接
Class SomeClass <T extends SuperClass & Interface1 &Interface2> { ...}
综上,Number就是类型参数的上界,T类型只能是Number和Number的子类。
通配符参数##
上例中,我们使用了getSum()方法来求不同数字类型的两个数之和,如果要扩展Caculator类,让其比较两个数之和是否相等。按照上面思路我们可以这样做:
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
Boolean isEquals(Caculator<T> caculator){
return this.sum == caculator.sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(3,4);
System.out.println(a);
Caculator<Double> doubleCaculator1 = new Caculator<>();
double d = doubleCaculator.getSum(3.0,4.0);
System.out.println(d);
//非法,因为T的类型为Integer,isEquals()方法中传入的类型为Caculator<String>.
integerCaculator.isEquals(doubleCaculator1);
Caculator<Integer> integerCaculator1 = new Caculator<>();
double e = integerCaculator.getSum(1,6);
System.out.println(e);
//合法,因为传入的类型参数都是Integer
integerCaculator.isEquals(integerCaculator1);
}
}
从上面的代码可以看出,上面的类值能适用于参数类型相等的情况,如果我们要比较3+4和2.5+4.5,此方法就不适用了。为了实现更好的一般性,我们可以使用通配符。
先来看一下用通配符怎么实现
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
//注意,这里修改了类型参数,把T换成了?
Boolean isEquals(Caculator<?> caculator){
return this.sum == caculator.sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(11,3);
System.out.println(a);
Caculator<Double> doubleCaculator = new Caculator<>();
double b = doubleCaculator.getSum(11.0, 3.0);
System.out.println(b);
System.out.println(doubleCaculator.isEquals(integerCaculator));
Caculator<Integer> integerCaculator2=new Caculator<>();
double c = integerCaculator2.getSum(2,12);
System.out.println(c);
System.out.println(integerCaculator.isEquals(integerCaculator2));
}
}
通配符----?,顾名思义就是可以给泛型类传递任何类型的类型参数,通配符对参数类型没有限制,限制条件是<T extends SuperClass>决定的。
有界通配符###
下图定义三个坐标类,FourD继承自TreeD,ThreeD继承自TwoD。就是说,四维坐标可以有三维坐标和二维坐标的行为,三维坐标可以有二维坐标的行为。
class TwoD {
int x;
int y;
public TwoD(int x, int y) {
this.x = x;
this.y = y;
}
}
class ThreeD extends TwoD {
int z;
public ThreeD(int x, int y, int z) {
super(x, y);
this.z = z;
}
}
class FourD extends ThreeD {
int t;
public FourD(int x, int y, int z, int t) {
super(x, y, z);
this.t = t;
}
}
Coords类把坐标类型限制在TwoD以及TwoD的子类。
BoundeWidecard类定义了三个打印坐标的方法:
- show2D(Coords<? extends TwoD> coords)方法可以打印TwoD、ThreeD和FourD中任意一个类的xy坐标。
- show3D(Coords<? extends ThreeD> coords)方法可以打印ThreeD和FourD中任意一个类的xyz坐标。
- show4D(Coords<? extends FourD> coords)方法可以打印FourD中的xyzt坐标。
class Coords<T extends TwoD> {
T ob;
Coords(T ob) {
this.ob = ob;
}
}
public class BoundeWildcard {
static void show2D(Coords<? extends TwoD> coords) {
System.out.println("2d, x: " + coords.ob.x + " y: " + coords.ob.y);
}
static void show3D(Coords<? extends ThreeD> coords) {
System.out.println("3d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z);
}
static void show4D(Coords<? extends FourD> coords) {
System.out.println("4d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z + "z: " + coords.ob.t);
}
public static void main(String[] args) {
Coords twoD = new Coords(new TwoD(11,22));
Coords threeD = new Coords(new ThreeD(111,222,333));
Coords fourD = new Coords(new FourD(1111,2222,3333,4444));
show2D(twoD);
show3D(threeD);
show4D(fourD);
//show3D(twoD); //运行时错误
show3D(threeD);
show3D(fourD);
//show4D(twoD); //运行时错误
//show4D(threeD); //运行时错误
show4D(fourD);
}
}
结果
2d, x: 11 y: 22
3d, x: 111 y: 222z: 333
4d, x: 1111 y: 2222z: 3333z: 4444
3d, x: 111 y: 222z: 333
3d, x: 1111 y: 2222z: 3333
4d, x: 1111 y: 2222z: 3333z: 4444
Process finished with exit code 0
泛型方法
下面我们创建一个泛型方法来判断一个对象是都存在于一个对象数组中。
public class GenMethDemo {
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y){
for (V v : y) {
if ((x.compareTo(v))==0)
return true;
}
return false;
}
public static void main(String[] args) {
int x =2;
Integer [] x_array = {1,2,3,4,5,6}; //不能用简单类型创建
Integer [] y_array = {1,3,4,5,6};
boolean flag;
flag = isIn(x,x_array);
System.out.println("2 is in x_array: " + flag);
flag = isIn(x,y_array);
System.out.println("2 is in y_array: " +flag);
}
}
说明参数类型T都要实现Comparable<T>泛型接口,这样才能使用compareTo方法来比较两个对象。V extends T表示V是T或者是T的子类,这样声明了兼容的数据类型,方便比较。
泛型构造函数##
可以将构造函数泛型化,即使不是泛型类。
public class GenConstructor {
double val;
<T extends Number> GenConstructor(T val){
this.val = val.doubleValue();
}
void showVal(){
System.out.println("val: "+ val);
}
public static void main(String[] args) {
GenConstructor genConstructor = new GenConstructor(11);
GenConstructor genConstructor1 = new GenConstructor(11.3f);
genConstructor.showVal();
genConstructor1.showVal();
}
}
结果:
val: 11.0
val: 11.300000190734863
Process finished with exit code 0
泛型接口##
一般来说,泛型接口的声明和泛型类的声明是一样的。值得注意的是,因为实现类需要实现泛型接口,所以实现类必须是泛型类。下面是一个泛型接口和实现的例子:
public interface GenInterface <T extends Comparable<T>> {
}
class Gen<T extends Comparable<T>> implements GenInterface<T>{
}
因为实现类实现了接口,实现类就必须和接口的参数类型相同(包含接口的参数类型),所以在implements子句中就不用再将参数类型全部写全,而且这样是错误的:
class Gen<T extends Comparable<T>> implements GenInterface<T extends Comparable<T>>{//非法
}
使用泛型接口有两个优势:
- 针对不同类型实现
- 可以给类型设置边界
使用泛型的一些限制
不能实例化类型参数###
class Gen<T>{
T ob;
Gen(){
this.ob = new T(); //非法
}
}
很明显,这里T只是一个占位符,系统不知道具体的类型。
对静态成员的一些限制###
class Wrong<T>{
static T ob; //非法,不能声明T类型的静态变量
//非法,静态方法返回值不能是T
static T getOb(){
return ob;
}
}
尽管不能声明某些带有类型参数的静态成员,但是可以声明泛型方法。下面的泛型方法用来判断一个对象是否存在于另外一个对象中,因为不能判断类型的具体类型,所以使用泛型方法,可以看出泛型方法的使用范围更加广泛,这就是泛型的“泛”的体现。
public class GenMethDemo {
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y){
for (V v : y) {
if ((x.compareTo(v))==0)
return true;
}
return false;
}
public static void main(String[] args) {
int x =2;
Integer [] x_array = {1,2,3,4,5,6}; //不能用简单类型创建
Integer [] y_array = {1,3,4,5,6};
boolean flag;
flag = isIn(x,x_array);
System.out.println("2 is in x_array: " + flag);
flag = isIn(x,y_array);
System.out.println("2 is in y_array: " +flag);
}
}
泛型类层次##
和非泛型类一样,泛型类可以继承和被继承。但是需要注意,和非泛型类不一样的是:泛型类需要向上传递超类需要类型参数,就想非泛型类中构造器向上传递一样。
泛型超类##
泛型超类示例:
public class Gen <T> {
public static void main(String[] args) {
SubGen<String> subGen = new SubGen<>();
}
}
class SubGen<T> extends Gen<T>{
}
下面的代码向超类传递了参数类型String,对于Gen来说,他的参数类型为String。
SubGen<String> subGen = new SubGen<>()
还要注意的是,泛型子类除了将类型参数传递给泛型超类,再也没有使用类型参数T。所以,即使泛型超类的子类不必泛型化,其也必须指定泛型超类需要的类型参数。
当然,泛型子类也可以添加自身的类型参数
class SubGen<T,V> extends Gen<T>{
}
泛型子类###
非泛型类可以是泛型类的超类。
public class Gen {
}
class SubGen<T> extends Gen{
}
强制转换
需要注意的是,使用强制转换时两个泛型类的类型必须兼容,并且类型参数也要相同。
类型擦除##
通常,我们不需要知道Java源码转化为对象代码的细节,但是对于泛型而言大致了解这个过程是很重要的,这有助于我们理解泛型的工作机制。
影响泛型如何添加到Java的一个最重要的约束:就是要和之前的Java版本兼容,简单的说就是要和之前的非泛型代码兼容,对Java语法和虚拟机做的修改不能破坏以前的代码。为了实现泛型,Java使用了类型擦除。
总的来说,运行时没有参数类型,所有的类型参数都会转化为具体的类型,如果没有界定类型,则用Object表示。
桥接方法###
有时候,编译器需要添加一些桥接方法,比如泛型子类重写方法的类型擦除,不能产生超类中方法的类型擦除。对于这种情况,会生成使用超类类型擦除的方法,并且这个方法调用具有由子类指定的类型擦除的方法。当然,桥接方法只会在字节码级别发生,你不会看到,也不能使用。
比如在父类中有一个getOb()方法
public class Gen<T> {
T ob;
Gen(T ob){
this.ob = ob;
}
T getOb(){
return ob;
}
}
但是这个泛型类的子类重写了该方法
class Gen2<String> extends Gen<String>{
Gen2(String ob) {
super(ob);
}
String getOb(){
System.out.println("aaaa");
return ob;
}
}
对于上面的两段代码,子类Gen2扩展了Gen,但是使用特定于String的Gen版本,同时Gen2还重写了getOb()方法。所有这些都是可以接受的,但是对于类型擦除却稍显麻烦。本来期待的下面的方法:
T getOb(){
return ob;
}
为了处理这个问题,编译器生成一个桥接方法,这个桥接方法调用String版本的那个签名。因此如果检查有javap生成的Gen2类文件,就会看到下面的方法:
java.lang.String getOb();
java.lang.Object getOb(); //桥接方法
对于上面两个方法,唯一不同的就是返回类型,通常来讲这是错误的,因为这不是源码引起的,JVM会正确的处理它。
对泛型数组的一些限制
不能实例化参数类型的数组,这和不能实例化参数类型的对象类似。
class Gen<T extends Number> {
T[] a;
// a = {1,2,3};//错误,不能初始化
Gen(T[] a){
// this.a = new T[10];//错误,不能实例化
this.a=a;//可以赋值
}
}
Java 8官方教程说指定类型参数的泛型数组不能实例化。
public class Restriction {
public static void main(String[] args) {
Integer[] integers = {1,2,3,4,5,6};
Gen<Integer> gen[] = new Gen<Integer>[10]; //编译错误
Gen<?> gen1[] = new Gen<Integer>[10];//可以通过
}
}
由于泛型内容比较多,如有遗漏请在讨论区补充,本文会持续更新修改,欢迎关注。