
一 、泛型产生背景:
1、背景
泛型是java5新增的语法糖特性。这种技术可以把编译期发生的错误提到运行期,很大程度上提升编码效率。
如类型转换ClassCastException,由于默认情况下向集合中添加的类型元素都是Object类型的,取出的也是Object类型的,所以集合汇总添加了不同类型的元素在取出时就容易发生类型转换的异常。然而这个异常在编译期是没有任何问题的,编译器会编译通过,但是在运行期就抛出异常了。通过增加泛型这个语法,使编译期间限制添加元素的类型,极大避免了类型转换的出现。
2、安全问题栗子
List list = new ArrayList();
list.add("abc");
list.add(123);
//编码时(编译器)不会报错,运行时(运行期)报错(ClassCastException)
Integer in =(Integer) list.get(0);
3、解决方案
jdk5 中的泛型允许程序员在编码时,限制集合的处理类型,吧原来运行期可能发生的问题转化到编译时的问题(编译期就会报错), 以此提高程序的可读性和稳定性。
如上:此时只允许你加入规定的类型,加入其他类型时,编译时就会报错,好吧泛型的好处不止于此,好多框架的设计,通用程序的封装都会使用泛型,接下来我们就在总结中体会下java的泛型之美。
二 、泛型的使用
1、系统定义的类或者方法:
- Java 的List系列集合
- Java的Map系列集合
遍历时建议多练习entryset, j2ee好多框架源码都是这种方式。
ps:java的集合+泛型的使用在我们日常开发必不可少,这里就不总结啦。。。
(1)还需注意
- 使用泛型时,泛型类型为引用类型 不能为基本类型。
- 泛型是提供给javac 编译器使用的,它用于限定集合(集合使用最多就一集合举例子)的输入类型,让编译器在源码的级别上挡住集合中插入的非法数据(运行期异常提升到编译期), 但编译器编译完java程序后,生成的class文件 是不带泛型信息的,以此使代码运行效率不受影响,这个过程被称为“擦除”。
2、 自定义泛型方法:
java 程序的普通方法,构造方法,静态方法都可以使用泛型。方法的使用之前要进行声明。
(1)语法栗子
注意
1、泛型声明通常放在方法的返回值之前,用两个尖括号包括一个大写字母。
2、T 可以使任意字母,但是一般都大写。
3、T就代表某一类型
4、T的作用范围是 整个方法期间(生命周期和方法一致)
5、T的类型由外界调用时指定(参考下面栗子demo方法调用)
(2)自定义泛型方法栗子:
package gegeric;
/**
* Create by SunnyDay on 2018/11/26
*/
public class GenericMethod {
public <T> void demo(T t) {
}
//返回值类型为T
// public <T> T demo(T t){
// return t;
// }
// 简单测试
public void test() {
demo("sdsf");
// String s = demo("sdsf");
}
}
1、这里我们传入的实参是字符串类型的值,则demo方法的T就代表String类型
2、此时有人或许疑问问什么方法内的参数不定义为Object类型(demo(Object obj))这样也可以接受任意类型。
原因: 当demo方法需要返回值类型呢(参看上文注释部分的代码),你用obj 避免不了要进行强制转换,安全问题可能发生。假如你定义泛型 可以直接返回值类型为T(如上test方法我们调用时是安全的 : String s = demo(“sdsf”)😉
(3)注意还可以定义多个泛型的方法:
和单个泛型使用都差不多。。。。
3、 自定义泛型类
如果一个类多处都要使用到同一个泛型,这时可以吧泛型定义到类上(即类级别的泛型)
语法:直接类名的后面 <大写字母(例如T)> 就行了
1、泛型类的使用和泛型方法类似T就是一个标记
2、同理T的作用范围是整个类体,整个类中都可以使用。
3、T同一可以为任意字母一般大写
4、T的类型由外界调用时指定
ps:泛型类的写法可以参考java的集合类源码
(1)语法栗子-定义泛型类
/**
* Created by sunnyday on 2021/5/20 10:49
*/
class BlockDemo<T> {
private final T t;
public BlockDemo(T t) {
this.t = t;
}
public T getT() {
return t;
}
public static void main(String[] args) {
// 1、使用泛型类型(限制为String)
BlockDemo<String> demo1 = new BlockDemo<>("demo1");
System.out.println(demo1.getT());
// 2、未使用泛型类型(未传递泛型实参,所以默认为Object类型)
BlockDemo demo2 = new BlockDemo(false);
System.out.println(demo2.getT());
// 上述例子和List 有无泛型例子一致。可参考理解
}
}
(2)语法栗子-定义泛型子类&不传递泛型实参
不传递泛型实际参数啥意思呢,就是子类也使用泛型,如Test<T>,如果子类定义为Test<String> 或者Test<Integer> 等传递实际类型的参数 就是传递泛型实际参数。
/**
* Created by sunnyday on 2021/5/21 16:27
* 泛型类的继承规则满足下列某一情况即可:
* 1、二者都指定泛型
* 2、二者都不执行泛型
* 3、其他情况报错(如一个指定一个不指定)
*/
class Test<T> extends BlockDemo<T> { //Test extends BlockDemo<T> 编译报错,无法识别的T类型. -> 会通过Test<T> extends BlockDemo<T>
public Test(T t) {
super(t);
}
}
(3)语法栗子-定义泛型子类&传递泛型实参
/**
* Created by sunnyday on 2021/5/21 16:27
* 注意:传入实参如String,这里就相当于指定了特定类型
* 1、 Test<String>这里的String可有可无
* 2、类中使用的泛型参数都要替换为String类型
*/
class Test extends BlockDemo<String> {
public Test(String s) {
super(s);
}
}
(2)类中静态方法的泛型
类上面定义的泛型只能作用类的非静态方法上面,静态的方法要单独声明泛型(如上Generic类的save方法,你把这个方法声明为静态的试试)
ps:声明后报错’generic.GenericMethod.this’ cannot be referenced from a static context
改写如下:只需要在返回值类型前添加泛型即可
static public <K> void save(K k){
System.out.println(k);
}
4、泛型接口
泛型接口与泛型类的定义及使用基本相同。
5、泛型通配符和上下边界
(1)为什么要用通配符呢?
如下:数组是可以的,泛型是不可以的。
形变:保持了子类型序关系≦。该序关系是:子类型≦基类型。
逆变:逆转了子类型序关系。
java中数组的协变的,因为父类的类型是确定的。子类无论如何继承实现都是父类的子类。
java中泛型是逆变的如List<T> T就是泛型,类型具有不确定性。如List<Animal> 与 List<Cat> 之间无法确定关系的,可能使用中List<Animal> 也会写成其他类型所以父类子类都具有不确定类型,根本无法确定两个类型之间的关系所以引入通配符来限定泛型。其实通配符就相当于泛型实参。
简单的总结:使用泛型就代表指定了特定类型。就向java类一样两种不同类型之间转换时会报错(java特定类之间转换失败:类转换异常),普通类之间建立继承或者实现关系就可以转换了。同理泛型类也是,建立继承或者实现的边界。
(2)通配符分类
-
无边界通配符<?>
泛型能够接受任意类型
-
固定上边界的通配符<? extends T>
泛型能够接受T类,及其子类类型(按照extends 的功能记忆就行)
-
固定下边界的通配符<? super T>
泛型能够接受指定T类,及其父类类型(按照super的功能记忆就行)
1、上述也就是面试长问的:泛型中?、super、extends的区别
2、上述的T就是边界
3、不能同时指定上下边界
ps:通配符其实是泛型实参、泛型实参、泛型实参、这里一定要注意,这样才能限制具体的类型范围。如果为形参还是不知道具体的类型范围。?通配符也当做Object理解即可。
6、泛型擦除-泛型擦除的时机
这种参数化类型之存在于编译阶段。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法,也就是说,泛型信息不会进入到运行期。
泛型是java语言的语法糖特性,在编码阶段我们可以使用,在javac编译代码期间生成字节码之前会有解语法糖的操作,这里会对泛型、可变参数、自动装箱拆箱进行原始代码的支持,然后生成字节码。
7、 实例练习
(1) 泛型方法
1 // 编写一个泛型方法实现指定位置数组元素交换
public <T> void swapArr(T[] arr ,int pos1,int pos2){
T temp =arr[pos1];
arr[pos1] = arr[pos2];
arr[pos2] = temp;
}
// 注意数组为通用数
2 // 编写一个泛型方法 接受任意数组 并颠倒数组中的元素
// 思路:定义两个指针,从前后开始一个加加一个减减,循环交换数组元素,指针相等结束循环。
public <T> void reverse(T[] arr) {
int startPoint = 0;
int endPoint = 0;
while (true){
if (startPoint>=endPoint){
break;
}
T temp = arr[startPoint];
arr[startPoint] = arr[endPoint];
arr[endPoint] = temp;
startPoint++;
endPoint--;
}
}
(2)泛型类
参考:深入探究数组 的通用数组
end
参考文章:
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题