Java进阶-泛型

随便扯点

我为什么要再次学Java

java知识是作为Android开发的语言基础,虽然现在已经推出了kotlin,但是基于以下原因我还是需要好好牢牢掌握java:
①SDK还是改成java,kotlin也需要编译成为java运行;
②目前大量的第三方库和继承与前任的代码都是java所写的;
③Java语言应用不仅仅在Android,就是在后台开发中也是一个最流行的语言;
④大公司面试都要求我们有扎实的Java语言基础。
所以,好好学Java!

一.为什么要用泛型

假如我们需要进行两个整型数字的相加:

	public int addInt(int a,int b) {
    
    
		return a+b;
	}

ok,很简单,那么如果需求变了,我们需要两个double型数据的相加,好,那就再写一个方法出来

	public double addDouble(double a,double b) {
    
    
		return a+b;
	}

当然,这很简单。但是在实际应用当中,很多情况下我们不一定事先知道我们需要的数据类型,难道我们要把所有数据类型的数字相加的方法都写一遍吗?这未免有点太扯犊子了。如果有那么一种类型,它可以接受所有的数据类型,那就太棒了!没错,这就是泛型。所以上面两个(或者说n多个)方法可以简化为一个函数(虽然也要进行类型的判断),但是达到了简化方法数目的目的。
OK,这样就引出了泛型的第一个好处
适用于多种数据类型执行相同的代码
那么继续来讲。我们都用过集合,存储数据,比如ArrayList,当你存入了一堆String数据的时候,再存入一个Int数据的时候,编译器不会显示任何错误,因为此时list默认的类型为Object类型,但是在运行的时候如果需要强转成String类型后输出,就会出错了。报错一般是

java.lang.ClassCastException

但是如果一开始就使用了泛型来规定存放的数据只能是String类型(尖括号里面加个String),那在编译阶段编译器就能显示错误,这样就方便多了。所以这里又得出了泛型的第二个好处:
泛型中的类型在使用时指定,不需要强制类型转换
泛型作用:
①相同的代码可以适应多种数据类型
②在编码的过程中就可以指定数据类型,而不需要进行强转,不至于编译的时候看不出来,运行的时候才报错。

二.什么是泛型

先看一段官方的理解:

泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

这一段话不理解无所谓,主要是会用,下面的知识要懂。

1.泛型类

(1)一个T

public class NormalGeneric<T> {
    
    
	private T data;
	public NormalGeneric(T data) {
    
    
		this.data = data;
	}
}

(2)一个T一个K

public class NormalGeneric<T,K> {
    
    
	private T data;
	private K num;
	public NormalGeneric(T data) {
    
    
		this.data = data;
	}
}

把T换成其他字母也行,但是最常用的是T、E、K、V这四个
这个太简单了,没啥可说的。

2.泛型接口

public interface Generator<T> {
    
    
	public T fun();
}

好,下面要睁大眼睛了。实现泛型接口的类有两种方式

(1)未指定T,实现类本身也是泛型类
public class JavaImpls<T> implements Generator<T>{
    
    
	public T fun() {
    
    
		return null;
	}
}
(2)指定了T
public class JavaImpls implements Generator<String>{
    
    
	public String fun() {
    
    //这里的T要换成String
		return "hello";
	}
}

这里要注意,fun函数的返回值类型就不能再是T了,而是你指定的类型,在这里就是String。

(3)泛型方法
public <T> T genericFun(T data){
    
    
		return data;
}

注意它和普通方法的区别

public String normalFun(String data) {
    
    
		return data;
}

判断方法是否为泛型方法,就看它是否有尖括号包裹,如果没有,就不是泛型方法,比如

	public NormalGeneric(T data) {
    
    
		this.data = data;
	}

这个方法只是使用了泛型作为参数,并非是泛型方法。类似的,如果只是以泛型作为返回值,那么它依然不是泛型方法。

注意:不是声明在泛型类里面的方法就是泛型方法。在泛型类中定义的泛型方法,即使表示符号都是T,那么泛型方法的T和泛型类中的T也是不一样的。“就近原则”

三.限定类型变量

有时候,我们需要对泛型加以约束,比如想使用compareTo方法,如果直接使用T.
是点不出来的,因为不知道T是什么类型,而如果我们能告诉编译器,甭管它是什么类型,反正它实现了Comparable接口,那么它就能用compareTo方法了。而这,就是对泛型的约束。

(1)一开始想要进行两个数字的比较

	public <T> T getMin(T a,T b) {
    
    
		if(a.compareTo(b) > 0){
    
    
			return b;
		}else {
    
    
			return a;
		}
	}

(2)但是这样编译是不通过的,如果我们对T加个限制,就可以了

	public <T extends Comparable> T getMin(T a,T b) {
    
    
		if(a.compareTo(b) > 0){
    
    
			return b;
		}else {
    
    
			return a;
		}
	}

如果这个时候,我们试图传入一个没有实现接口Comparable的类的实例,将会发生编译错误。这也是合理的。
注意:①extends左右都允许有多个,如 T,V extends Comparable & Serializable
②注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。
比如

	public <T extends Number&Comparable> T getMin(T a,T b) {
    
    
		if(a.compareTo(b) > 0){
    
    
			return b;
		}else {
    
    
			return a;
		}
	}

Java单继承多接口,当泛型继承接口和类时,只能有一个类,而且要把类放在最前面,后面有多少接口都可以,中间用&连接

四.泛型的缺点

(1)不能用基本类型实例化“类型参数”(也就是泛型)

		//Restrict<double> a = new Restrict<>();
		Restrict<Double> a = new Restrict<>();

(2)运行时类型查询只适用于原始类型

不能用instanceof这个关键字。

		Restrict<Double> a = new Restrict<>();
		//下面两种不行
		//if(a.instanceof Restrict<Double>) {}
		//if(a.instanceof Restrict<T>) {}
		Restrict<String> b = new Restrict<>();
		if(a.getClass() == b.getClass()) {
    
    }

最后这里,是true,打印.getName()是Restrict。也就是说不管传入的是什么类型的泛型,getClass获得的都是泛型类的原生类型

(3)泛型类的静态上下文中类型变量失效

也就是说别在静态域或者静态方法里引用泛型

因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。

(4)不能创建参数化类型的数组

也就是说不能有泛型类型的数组,像new Array《Double》[10]这种就不行。可以声明数组,但是不能new。

(5)不能实例化类型变量

即不能new T()

(6)不能捕获泛型类的实例

泛型类不能extends:Exception/Throwable(并不是说T不能extends这些)

		try {
    
    }
		catch(T e) {
    
    
		}

但是可以这样使用

public <T extends Throwable> void doWorkSuccess(T x) throws T{
    
    
		try {
    
    }
		catch(Throwable e) {
    
    
			throw x;
		}
	}

注意:这里仍然没有捕获泛型类的实例。

五.通配符(WildChar)“?”

比如Worker类继承Employee类,Pair类是个泛型类,那么问题来了,
Pair<Employee>类是Pair<Worker>类的父类吗?
我们输入一下代码看看:在这里插入图片描述
很显然这里编译就出错了。结论是两者没有任何的继承关系

但是泛型类可以继承或者扩展其他泛型类,比如List和ArrayList

但是仅仅是这样不能满足我们的很多要求,如果Pair<Worker>能传进去那就方便多了。所以在这里就有了通配符类型。具体为:

? extends 父类。限制了传入类型变量(泛型)的上界
? super 子类。限制了传入类型变量(泛型)的下界

注意:通配符只能在泛型方法上用,一般出现在参数中。
举例:某四个类的继承关系为:HongFuShi继承Apple,Apple和Orange都继承Fruit。
那么如果想要Pair<Fruit>参数接受Pair<Apple>参数,就要把前者变为Pair<? extends Fruit>

这个时候如果想使用Pair的get方法是可以的,因为不管你传进来的是Apple还是Orange或者是其他的子类,编译器都可以很确切地明白它是Fruit。但是使用set方法就不行了,因为不知道你第一次传进来的是Fruit类还是Apple级别的类还是更深的子类,盲目地set就可能产生类型不一致的错误。所以extends主要是用于安全的访问数据。
总结:主要用于安全地访问数据,可以访问extends后面的类及其子类型,并且不能写入非null的数据。

正是因为有了这个问题,才产生了super。比如<? super Apple>

这里就可以使用Pair的set方法了,但是注意:只能set 方法传入的参数只能是Apple本身或者子类,而不能传入它的超类(我个人的理解是传入子类可以转换为Apple)死记吧!这里也能用get方法,只不过返回的是Object类型(Apple非常确定的超类)而不是像前面那个返回Fruit类型(下界)。
总结:主要用于安全地写入数据,可以写入extends后面的类及其子类型。

六.Java虚拟机如何实现泛型(伪泛型)

Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

总之就是使用原生类型来替换T,比如T啥都不继承的时候,就把T转化为Object,如果T继承了类和接口,那就把T替换成类(此时这个类为原生类型),当用到接口的方法的时候,就进行一个强转。

和C#中泛型的区别:

泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型

猜你喜欢

转载自blog.csdn.net/afdafvdaa/article/details/109450381