目录
十二、泛型的使用场景,可以作用在什么位置,使用泛型有什么好处
有意者可加
作者:研J小政
课堂:wclass
(有什么弊端请私信我,目前参考众多资料精华整理过程过中)
章节:主要带领大家彻底认识 泛型设计的绝大部分盲点,让大家学习 泛型设计 章节无弯路可走!
QQ:3544839383
资料学习群:
进度:持续更新迭代!
录课状态:待录
参考文献
- 泛型的使用场景,可以作用在什么位置,使用泛型有什么好处
- 简述泛型的基本使用和作用,Java面试常见问题解析
- 泛型格式
- 泛型(详解加示范)
- 什么是泛型
- 什么是泛型?
- 泛型是什么,为什么要用泛型
- 使用泛型的主要原因
- 为什么要用泛型呢
- 反射和泛型
- 深入理解Java泛型及其在实际编程中的应用
- 泛型在项目中的使用
- Java 高级特性之泛型
- java 泛型解析
一、什么是泛型,泛型有什么用,为什么要用
1. 说法一
1.1 什么是泛型
泛型就是指在类定义时不会设置类中的属性或方法参数的具体类型,
而是在类使用时(创建对象)再进行类型的定义。
会在编译期检查类型是否错误。
类声明后的<>中这个T被称为类型参数,用于指代任意类型,实际上这个T只是个代表,写什么都可以。
表示此时的value1,value2都是在类定义时没有明确类型,只有在使用时才告知编译器类型。
出于规范,类型参数用单个的大写字母来代替,常见如下:
- T:代表任意类
- E:表示Element的意思,或是异常
- K:与V搭配使用
- V:与K搭配使用
1.2 泛型的使用
若此时value1和value2的类型不一定相同,就定义多个类型参数:
注意:非静态内部类会复用外部类的泛型参数,静态内部类不会复用外部类的泛型参数。
1.3 为什么要用泛型
1. 保证了类型的安全性
泛型约束了变量的类型,保证了类型的安全性。
例如List和ArrayList。List集合只能加入int类型的变量,ArrayList可以Add任何常用类型,编译的时候不会提示错误。
2. 避免了不必要的装箱、拆箱操作,提高程序的性能
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
举例说明:
使用泛型之前,我们使用object代替。
object a=1;//由于是object类型,会自动进行装箱操作。
int b=(int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。
使用泛型之后:
public static T GetValue<T>(T a){
return a;
}
public static void Main(){
int b=GetValue<int>(1);//使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。
}
3. 提高方法、算法的重用性
上面的例子基本能说明这个优势
2. 说法二
2.1 什么是泛型,泛型有什么用,为什么要用
- 泛型就是一种未知的类,将未知的类型声明在集合、对象上,泛型的默认类型为Object。例如: ArrayList str = new ArrayList();这是将String类型指定在str这个集合内,这个集合存储或者读取的类型只能为String类型。
- 使用泛型可以在传参的时候(使用占位符 ? 占位)不固定传入什么类型,即可通用类型,如果不设置泛型的上限(例如:< ? extends List>)和下限(例如:<? super List>)。
- 使用泛型后对象或者集合内只能放入指定的数据类型,可以保证对象或者集合的安全性,减少类型的转换操作。
2.2 怎么使用泛型,泛型可以作用在什么位置
泛型只能定义引用数据类型,而不能使用基本数据类型
泛型类、泛型方法、泛型接口、泛型通配符
例如:
- 作用在类上时( public class Animal { E pet; } ) , 泛型跟在类后边,可以指定用了泛型的类内部的 pet 的类型。
- 作用在对象上时( Animal str = new Animal(); ) , 泛型跟在类后边 , 使得对象类的的 pet 属性为 Dog类型。
- 作用在方法上时( public Animal getPet(){ return E ; } ), 如在类上没有声明泛型时,必须在返回值和访问修饰符之间声明。
- 作为方法入参时( public void setPet(E pet){ this.pet = pet ; } ), 如在类上没有声明泛型时,必须在返回值和访问修饰符之间声明
2.3 使用泛型的好处
- 使用泛型后对象或者集合内只能放入指定的数据类型,避免出现对象或者集合内的对象在多态使用的时候出现类型转换异常
(java.lang.ClassCastException),可以保证对象或者集合的安全性。
- 指定了类型后,对象、集合或方法内只能使用对应的类型,可以减少类型的转换操作(在没有指定类型是类型转换必须使用 instanceof 关键字来进行判定),缩减了代码了,方便了程序员。
二、泛型格式
- <类型>:指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母。
比如:、、、
- <类型1,类型2…>:指定多种类型的格式,多种类型之间用逗号隔开。例如: <E,T> <K,V>
三、使用泛型的好处
- 把运行时期的问题提前到了编译期间
- 避免了强制类型转换(如果不写泛型那么可以存入Object类型,假如我存入了Stirng和int类型那么我在用.length方法就需要强制转换)
为什么说运行时期的问题提前到了编译期间呢?
通俗来说我们在定义方法的时候会定义一个形参, 等待用户传过来一个实参, 我们通常使用确定的类型当作形参, 这
样就可能出现一种类型转换异常问题,就会出现运行时期异常ClassCastException异常(类型转换异常)
(因为以前默认采取Object类型,可以存放任意类型数据)
例如:
一个集合接收字符串又接收整型类型,获取取来我们都以为是字符串,从而导致报错,但是现在我们也可以让用户在
使用的时候才去定义到底应该传什么类型的参数,所以泛型就是在接收参数的时候定义该参数的类型,即传入过
来的那个参数的类型,这样我们就将运行时期异常提到了编译时期。既然我规定了该集合存储元素为字符串类型, 那
么该集合就只能存储字符串类型元素,否则存储其他类型元素就编译期间报错,如下用字符串集合添加整型元素报
错。
四、泛型类
1. 简介
泛型类(generic class)就是有一个或多个类型变量的类。常见的做法是类型变量使用大写字母,而且很简短。
Java库使用变量日表示集合的元素类型,K和V分别表示表的键和值的类型。
T(必要时还可以用相邻的字母U和S)表示“任意类型”。
泛型类就是说定义类时同时定义了泛型的类就是泛型类
2. 格式
饰符 class 类名<泛型变量>{ }
例如:public class MyArrayList { }
泛型变量T可以随便写为任意标识, 常见的如E、T、K、V等
3. 作用
泛型类的作用就是编译阶段可以指定数据类型, 类似于集合的作用
4. 泛型类的原理
泛型类的原理就是把出现泛型变量的地方全部替换成传输的真实数据类型
5. 从泛型类派生子类
- 子类也是泛型类,子类和父类的泛型类型要一致
class A<T> extends Demo<T>
- 父类是泛型类(没指定类型)子类继承父类后,也是泛型类,子类定义类型后,父类类型也随之明确
- 子类不是泛型类,父类要明确泛型的数据类型也就是说,子类继承一个泛型类:如果子类 没有定义泛型,那么父类的类型必须在声明时就要明确下来
class A extends Demo<String>
6. 演示示例
6.1 自定义泛型类并测试
1. 泛型类
public class Generic<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
2. 测试类
public class GenericDemo1 {
public static void main(String[] args) {
Generic<String> g1 = new Generic<String>();
g1.setT("张三");
System.out.println(g1.getT());
Generic<Integer> g2 = new Generic<Integer>();
g2.setT(30);
System.out.println(g2.getT());
Generic<Boolean> g3 = new Generic<Boolean>();
g3.setT(true);
System.out.println(g3.getT());
}
}
7. 总结
- 泛型的类型参数只能是类类型。
- 泛型的类型参数可以有多个。
- 如果没有定义具体类型,默认为Object。
五、泛型方法
1. 概述
定义方法时同时定义了泛型的方法就是泛型方法
2. 格式
修饰符 <泛型变量> 方法返回值 方法名称(形参列表){}
例如:
public <T> void show(T t) { }
3. 作用
泛型方法作用就是方法中可以使用泛型接收一切实际类型的参数,让方法更具备通用性
4. 泛型方法的原理
泛型方法的原理就是把出现泛型变量的地方全部替换成传输的真实数据类型
5. 演示示例
5.1 自定义泛型方法
带有泛型方法的类
public class Generic {
public <T> void show(T t) {
System.out.println(t);
}
}
测试类
public class GenericDemo2 {
public static void main(String[] args) {
Generic g = new Generic();
g.show("张三");
g.show(30);
g.show(true);
g.show(12.34);
}
}
六、泛型接口
1. 概述
使用了泛型定义的接口就是泛型接口
注意:泛型接口跟泛型类类似,子类不是泛型类,父类要明确泛型的数据类型。
2. 格式
修饰符 interface 接口名称<泛型变量>{}
例如:
public interface Generic<T>{}
3. 作用
泛型接口的作用就是可以让实现类选择当前功能需要操作的数据类型
4. 泛型接口的原理
泛型接口的原理就是实现类可以在实现接口的时候传入自己操作的数据类型,这样重写的方法都将是针对于该类
型的操作
5. 演示示例
5.1 自定义泛型接口
1. 泛型接口
public interface Generic<T> {
void show(T t);
}
2. 泛型接口实现类1
定义实现类时,定义和接口相同泛型,创建实现类对象时明确泛型的具体类型
public class GenericImpl1<T> implements Generic<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
3. 泛型接口实现类2
定义实现类时,直接明确泛型的具体类型
public class GenericImpl2 implements Generic<Integer>{
@Override
public void show(Integer t) {
System.out.println(t);
}
}
4. 测试类
public class GenericDemo3 {
public static void main(String[] args) {
GenericImpl1<String> g1 = new GenericImpl<String>();
g1.show("张三");
GenericImpl1<Integer> g2 = new GenericImpl<Integer>();
g2.show(30);
GenericImpl2 g3 = new GenericImpl2();
g3.show(10);
}
}
七、类型通配符
1. 什么是类型通配符?上限?下限?
- 类型通配符: <?>
-
- ArrayList<?>:表示元素类型未知的ArrayList,它的元素可以匹配任何的类型但是并不能把元素添加到ArrayList中了,获取出来的也是父类类型
- 类型通配符上限:<? extends 类型>
-
- ArrayListList <? extends Number>: 它表示的类型是Number或者其所有子类型
- 类型通配符下限:<? super 类型>
-
- ArrayListList <? super Number>: 它表示的类型是Number或者其所有父类型
2. 演示示例
2.1 看看类型通配符的作用?
public class GenericDemo4 {
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
//前提条件Integer继承Number又继承Object
ArrayList<String> list2 = new ArrayList<>();
ArrayList<Number> list3 = new ArrayList<>();
ArrayList<Object> list4 = new ArrayList<>();
method(list1);
method(list2);
method(list3);
method(list4);
getElement1(list1);
getElement1(list2);//报错
getElement1(list3);
getElement1(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型通配符: 此时的泛型?,可以是任意类型
public static void method(ArrayList<?> list){}
// 泛型的上限: 此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(ArrayList<? extends Number> list){}
// 泛型的下限: 此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(ArrayList<? super Number> list){}
}
八、泛型和虚拟机(类型擦除)
1. 简介
Java 虚拟机(JVM,Java Virtual Machine)中并不存在泛型, Java 语言中的泛型只在程序源码中存在,在编译
后的字节码文件(Class 文件)中, 全部泛型都被替换为原始类型,并且在相应的地方插入了 强制转型代码以及
对 8 大基本类型的 自动装箱和拆箱。这样做的 主要目的是为了兼容以前的版本(泛型是在 JDK 1.5 之后才被引
入 Java 中的,也就是说,在此之前 Java 并没有泛型的特性)。当然,利用这种方式实现泛型,所带来的不可避
免的后果就是执行性能的下降(Java 选择这样的泛型实现,是 出于当时语言现状的权衡,而不是语言先进性或者
设计者水平不够原因,如果当时有充足的时间好好设计和实现,是完全有可能做出更好的泛型系统的)。
2. 类型擦除
既然 JVM 中不存在泛型类型的对象,那么 Java 的泛型在 JVM 中又是如何定义的呢?答案是:类型擦除。
Java 的每个泛型类型都对应着一个相应的原始类型,原始类型用第一个限定的类型变量来替换, 如果没有给定限
定就用 Object 替换。
例如:
泛型类型 | 原始类型 |
ArrayList | ArrayList |
T | Object |
T extends Person & Comparable | Person |
类型擦除简单的来说,就是擦除原有的泛型类型,并用原始类型进行代替。
具体外面可以看一个例子:
public class Person<T> {
private T information;
public Person() {
this(null);
}
public Person(T information) {
this.information = information;
}
public void setInformation(T information) {
this.information = information;
}
public T getInformation() {
return information;
}
}
对于上述的 Person 类,类型擦除后的原始类型如下所示:
// 类型擦除后的Person类
public class Person {
// 泛型类型Person<T>被原始类型Person代替
// 类型变量T被 Object 代替
private Object information;
public Person() {
this(null);
}
public Person(Object information) {
this.information = information;
}
public void setInformation(Object information) {
this.information = information;
}
public Object getInformation() {
return information;
}
}
一个泛型类型(如 Person),经类型擦除后,就变成了原始的类型(Person)。
这样 JVM 就可以 “认识” 它了,这就解决了 JVM 中不存在泛型类型对象的限制。
3. 翻译泛型表达式
类型擦除解决了 JVM 中不存在泛型类型对象,但却又引出了一个新问题,请看下面的例子:
Person<String> person = new Person<>("泛型");
String information = person.getInformation();
这是一段很简单的代码,第一行实例化了一个 Person 的对象,并在第二行读取了它的信息,将信息赋值给一个
String 类型的变量。整个代码看起来并没有什么特殊的地方,但如果你理解了类型擦除的机制,可能会对第二行
的代码有些疑惑。
正如前面所说,类型擦除之后所有的类型变量均会被 Object 类型代替,即调用 person.getInformation () 得到
的应该是一个 Object 类型
的对象。而在第二行代码中我们直接让一个 String 类型的变量引用了它(没有经过强制类型转换)。
实际上,当程序调用泛型方法时,编译器会自动的帮我们插入强制类型转换
(使用泛型数据的方法都是泛型方法)。
在上述了例子中,擦除 getInformation()的返回类型后会返回 Object 类型的对象,然后编译器自动的插入了
String 的强制类型转换。
也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
对原始方法 person.getInformation () 的调用(返回一个 Object 类型的对象)。
将返回的 Object 类型的对象强制转换为 String 类型
除此之外,当存取一个泛型域时,编译器也会自动插入强制类型转换(如果这个域可以被外部访问到的话)。
假设 Person 类的 information 变量是 public 的(这不是种好的编程风格),表达式:
String information = person.information;
也会在结果字节码中插入强制类型转换。
4. 桥方法
类型擦除还会带来一个问题,我们继续以 Proson 类为例子:
public class Person<T> {
private T information;
public void setInformation(T information) {
this.information = information;
}
public T getInformation() {
return information;
}
}
有一个类 MyPerson,它继承了 Person 类,如果在 MyPerson 类中对 Person 类的方法进行重写,就会引起一
些问题,
例如:
public class MyPerson extends Person<String> {
// 参数通过继承的泛型类确定为String。
@Override
public void setInformation(String information) {
super.setInformation(information);
}
// 在MyPerson类中,Person中继承的2个方法均重写。
// 返回值通过继承的泛型类确定为String。
@Override
public String getInformation() {
return super.getInformation();
}
}
MyPerson 类继承了 Person 类的两个方法,并对它们进行了覆盖重写。
在 JVM 中,经过类型擦除后,以 setInformation 方法为例,Person 类中的是一个需要 Object 类型参数的方
法,而 MyPerson 类中的是一个需要 String 类型参数的方法,它们显然不是同一个方法(详见类与对象 ——5.
方法)。
这里, 同样希望方法的调用具有多态性, 并调用最合适的那个方法。
即如果是 MyPerson 类型的对象,就让他调用 MyPerson 类中的方法,而 Person 类型的对象则调用 Person<
String > 类中的方法。
要解决此问题,就需要编译器在 MyPerson 类中生成一个桥方法(bridge method):
public void setInformation(Object information) {
setInformation((String) information);
}
有了桥方法,我们就可以在泛型中实现多态:
Person<String> person = new MyPerson();
person.setInformation("泛型中的多态");
上述第二行代码会调用 MyPerson 类中的 setInformation 方法,实现了方法调用的多态性。
如果我们要进一步深究的话,这里还有一个问题,getInformation 方法怎么办
(我们知道一个类中不允许存在多个仅有返回类型不同的同名方法)。
如果继续利用桥方法,就会得到下面两个同名的方法,它们只有返回类型是不同的:
public String getInformation() {...}
public Object getInformation() {return getInformation()}
当然,我们不能编写这样的 Java 代码,但是,在 Java 虚拟机中,实际是通过参数类型和返回类型来确定一个方
法的。也就是说,当编译器产生两个仅返回类型不同的方法字节码时,Java 虚拟机能够正确地处理这一情况。
最后,我们再来谈谈继承中的覆盖重写。桥方法不仅仅被用于泛型当中,在继承中,当一个方法覆盖另一个方法
时,可以指定一个更严格的返回类型(详见面向对象程序设计 ——2.2.2 覆盖),这里其实也用到桥方法。具体原
理与前面类似,便不再赘述。
5. 总结
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。
那是因为,泛型信息只存在于代码编译阶段,在进入JVM之 前,与泛型相关的信息会被擦除掉,我们称之为一类
型擦除。
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
简单来说
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。
这个原始类型的名字就是去掉类型参数后的泛型类型名。
类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为 0bject)。
更简单的来说
JVM 中没有泛型,只有普通的类和方法。
在 JVM 中所有的类型参数都用它们的限定类型替换。
桥方法被合成来保持多态。
为保持类型安全性,必要时插人强制类型转换。
最后,需要注意的是,擦除的类其实仍然保留了一些泛型祖先的微弱记忆。
例如, 擦除后原始的 Person 类知道它源于泛型类 Person
(但无法区分是由 Person< String > 构造的还是由 Person< Double > 构造的)
九、泛型限定与局限性
1. 不能用基本类型参数化实例类型
即,没有Pair,Pair等类型
2. JVM中不存在泛型
运行时类型检查只适用于原始类型,即JVM中不存在泛型或指定类型的泛型,导致无法进行某些判断
ex. if(a instanceof Pair<String>) ; //--- error
无法判断a是Pair的一个实例,因为运行时,类型擦出后变pair,无法判断是否为String类型;
同理,强制类型转换时也会出现同样的问题
ex. Pair<String> p = (Pair<String>) a; //error
(Pair) a运行时无法得知String类型,只能得到Pair,导致转换异常
不同类型泛型擦出验证,以下代码说明JVM时是不区分泛型类型的,只保留原始类型
Pair<String> p1 = ...;
Pair<Integer> p2 = ...;
if(p1.getClass()==p2.getClass()); //true
3. 不能创建参数化类型的数组
pair<String> p = new Pair<Sting>[10]; //error
因为擦除机制,运行时无法区分p的引用内容(泛型具体类型),导致出现"猫插入狗列表"等业务逻辑上的异
常,因此java得设计者阻止了这种应用.
4. Varargs警告
Varargs警告基于java不允许创建参数化类型数组,但是允许可变参数引用的情况(此时的参数为类型参数)。
以下理解为抑制ts数组的警告
@SafeVarargs
Public static <T> void addAll(Collection<T> coll, T ... ts){ //ts理解为数组
for(t:ts)coll.add(t);
}
5. 不能实例化类型变量
不能实例化类型变量,new T(...), new T[...], T.class, JVM时变成new Object(...), new Object[...],
Object.class,业务上不可能存在这样的调用,所有java阻止了这种应用.
但是通过引用传递的方式是可以到达某种类似效果的.
比如:
Public static <T> pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.newInstance,cl.newInstance);
}catch(Exception e){
return null;
}
}
...
Pair<String> p = Pair.makePair(String.class);
6. 禁止使用带有类型参数的静态域或方法
禁止使用带有类型参数的静态域或方法,JVM时可能会遇到数据插入混乱问题,java阻止了这种应用.
7. 不能抛出或捕获泛型类的实例
不能抛出或捕获泛型类的实例,甚至连扩展Throwable都是不允许的.
public class Problem<T> extends Exception{ //error, can't extends Throwable
...
}
public static <T extends Throwable> void doWork(Class<T> t){
try{
do Work;
}catch(T e){ //error, can't catch type variable
Logger.global.info(...)
}
}
//不过在异常规则允许下是可以用到类型参数的
public static <T extends Throwable> void doWork()T t throw T //OK
{...}
8. 泛型擦除后的冲突
注意泛型擦除后的冲突,所以不能再泛型类中建立类似equals(T x)这样的方法,
因为擦出后与Object.equals(object obj)冲突了(相同的方法参数与方法名).
当两个接口是同一个接口的不同参数化时,要强行限制一个类或类型变量不能同时成为这两个类的子类,
即不能同时extends或implements这两个接口(有可能与中间合成的桥方法冲突).
十、泛型与反射
1. 概述
从 JDK5 以后,Java 的 Class 类增加了泛型功能,从而允许使用泛型来限制 Class 类。
例如:String.class 的类型实际上是 Class,如果 Class 对应的类暂时未知,则使用 Class<?>。
通过在反射中使用泛型,可以避兔使用反射生成的对象需要强制类型转换。
2. 泛型和 Class 类
使用 Class 泛型可以避免强制类型转换。
例如:下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例。
public class CrazyitObjectFactory {
public static Object getInstance(String clsName) {
try {
// 创建指定类对应的Class对象
Class cls = Class.forName(clsName);
// 返回使用该Class对象所创建的实例
return cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
上面程序中两行粗体字代码根据指定的字符串类型创建了一个新对象,但这个对象的类型是 Object,
因此当需要使用 CrazyitObjectFactory 的 getInstance() 方法来创建对象时,将会看到如下代码:
// 获取实例后需要强制类型转换
Date d = (Date)Crazyit.getInstance("java.util.Date");
甚至出现如下代码:
JFrame f = (JFrame)Crazyit.getInstance("java.util.Date");
上面代码在编译时不会有任何问题,但运行时将抛出 ClassCastException 异常,因为程序试图将一个 Date 对象
转换成 JFrame 对象。
如果将上面的 CrazyitObjectFactory 工厂类改写成使用泛型后的 Class,就可以避免这种情况。
public class CrazyitObjectFactory2 {
public static <T> T getInstance(Class<T> cls) {
try {
return cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
// 获取实例后无须类型转换
Date d = CrazyitObjectFactory2.getInstance(Date.class);
JFrame f = CrazyitObjectFactory2.getInstance(JFrame.class);
}
}
在上面程序的 getInstance() 方法中传入一个 Class 参数,这是一个泛型化的 Class 对象,调用该 Class 对象的
newInstance() 方法将返回一个 T 对象,如程序中粗体字代码所示。
接下来当使用 CrazyitObjectFactory2 工厂类的 getInstance() 方法来产生对象时,无须使用强制类型转换,系
统会执行更严格的检查,不会出现 ClassCastException 运行时异常。
前面介绍使用 Array 类来创建数组时,曾经看到如下代码:
// 使用 Array 的 newInstance 方法来创建一个数组
Object arr = Array.newInstance(String.class, 10);
对于上面的代码其实使用并不是非常方便,因为 newInstance() 方法返回的确实是一个 String[] 数组,而不是简
单的 Object 对象。
如果需要将对象当成 String[] 数组使用,则必须使用强制类型转换——这是不安全的操作。
为了示范泛型的优势,可以对 Array 的 newInstance() 方法进行包装。
public class CrazyitArray {
// 对Array的newInstance方法进行包装
@SuppressWarnings("unchecked")
public static <T> T[] newInstance(Class<T> componentType, int length) {
return (T[]) Array.newInstance(componentType, length); // ①
}
public static void main(String[] args) {
// 使用CrazyitArray的newInstance()创建一维数组
String[] arr = CrazyitArray.newInstance(String.class, 10);
// 使用CrazyitArray的newInstance()创建二维数组
// 在这种情况下,只要设置数组元素的类型是int[]即可。
int[][] intArr = CrazyitArray.newInstance(int[].class, 5);
arr[5] = "疯狂Java讲义";
// intArr是二维数组,初始化该数组的第二个数组元素
// 二维数组的元素必须是一维数组
intArr[1] = new int[] { 23, 12 };
System.out.println(arr[5]);
System.out.println(intArr[1][1]);
}
}
上面程序中粗体字代码定义的 newInstance() 方法对 Array 类提供的 newInstance() 方法进行了包装,将方法签
名改成了 public static T[] newInstance(Class componentType, int length),这就保证程序通过该
newInstance() 方法创建数组时的返回值就是数组对象,而不是 Object 对象,从而避免了强制类型转换。
提示:程序在①行代码处将会有一个 unchecked 编译警告,所以程序使用了 @SuppressWarnings 来抑制这个
警告信息。
3. 使用反射来获取泛型信息
通过指定类对应的 Class 对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用 private 修饰,还是
使用 public 修饰。
获得了成员变量对应的 Field 对象后,就可以很容易地获得该成员变量的数据类型,即使用如下代码即可获得指定
成员变量的类型。
// 获取成员变量 f 的类型
Class<?> a = f.getType();
但这种方式只对普通类型的成员变量有效。如果该成员变量的类型是有泛型类型的类型,
如 Map<String, Integer> 类型,则不能谁确地得到该成员变量的泛型参数。
为了获得指定成员变量的泛型类型,应先使用如下方法来获取该成员变量的泛型类型。
// 获得成员变量 f 的泛型类型
Type gType = f.getGenericType();
然后将 Type 对象强制类型转换为 ParameterizedType 对象,ParameterizedType 代表被参数化的类型,也就是
增加了泛型限制的类型
ParameterizedType 类提供了如下两个方法。
- getRawType():返回没有泛型信息的原始类型。
- getActualTypeArguments():返回泛型参数的类型。
下面是一个获取泛型类型的完整程序。
public class GenericTest {
private Map<String, Integer> score;
public static void main(String[] args) throws Exception {
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
// 直接使用getType()取出的类型只对普通类型的成员变量有效
Class<?> a = f.getType();
// 下面将看到仅输出java.util.Map
System.out.println("score的类型是:" + a);
// 获得成员变量f的泛型类型
Type gType = f.getGenericType();
// 如果gType类型是ParameterizedType对象
if (gType instanceof ParameterizedType) {
// 强制类型转换
ParameterizedType pType = (ParameterizedType) gType;
// 获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:" + rType);
// 取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型信息是:");
for (int i = 0; i < tArgs.length; i++) {
System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
}
} else {
System.out.println("获取泛型类型出错!");
}
}
}
上面程序中的粗体字代码就是取得泛型类型的关键代码。运行上面程序,将看到如下运行结果:
score的类型是:interface java.util.Map
原始类型是:interface java.util.Map
泛型信息是:
第0个泛型类型是:class java.lang.String
第1个泛型类型是:class java.lang.Integer
从上面的运行结果可以看出,使用 getType() 方法只能获取普通类型的成员变量的数据类型:
对于增加了泛型的成员变量,应该使用 getGenericType() 方法来取得其类型。
提示:Type 也是 java.lang.reflect 包下的一个接口,该接口代表所有类型的公共高级接口,Class 是 Type 接口
的实现类。
Type 包括原始类型、参数化类型、数组类型、类型变量和基本类型等。
十一、演示示例
1. 泛型类
1.1 Box
package com.zheng.collection.d3_genericity.genericityclass;
//就是一个泛型类
public class Box<E> {
private E element;
public E getElement() {
return element;
}
public void setElement(E element) {
this.element = element;
}
}
1.2 自定义泛型类
package com.zheng.collection.d3_genericity.genericityclass;
/**
* 自定义泛型类
*/
public class MyGenericityClass {
public static void main(String[] args) {
Box<String> box1 = new Box<>();
box1.setElement("给小丽的土味情话");
String element1 = box1.getElement();
System.out.println(element1);
Box<Integer> box2 = new Box<>();
box2.setElement(19);
Integer element2 = box2.getElement();
System.out.println(element2);
}
}
2. 类型通配符
package com.zheng.collection.d3_genericity.genericityglobbing;
/**
* 类型通配符:<?>
* ArrayList<?>:表示元素类型未知的ArrayList,它的元素可以匹配任何的类型
* 但是并不能把元素添加到ArrayListList中了,获取出来的也是Object类型
*
* 类型通配符上限:<? extends 类型>
* ArrayList<? extends Number>:它表示的类型是Number或者其子类型
*
* 类型通配符下限:<? super 类型>
* ArrayList<? super Number>:它表示的类型是Number或者其父类型
*/
public class genericityglobbing1 {
public static void main(String[] args) {
}
}
3. 泛型接口
package com.zheng.collection.d3_genericity.genericityinterface;
public class GenericityInterface {
public static void main(String[] args) {
GenericityImpl1<String> genericity = new GenericityImpl1<>();
genericity.method("小丽给我的土味情话");
GenericityImpl2 genericityImpl2 = new GenericityImpl2();
genericityImpl2.method(19);
}
}
interface Genericity<E>{
public abstract void method(E e);
}
class GenericityImpl2 implements Genericity<Integer>{
@Override
public void method(Integer integer) {
System.out.println(integer);
}
}
class GenericityImpl1<E> implements Genericity<E>{
@Override
public void method(E e) {
System.out.println(e);
}
}
4. 泛型方法
4.1 GenericityMethod1
package com.zheng.collection.d3_genericity.genericitymethod;
import java.util.ArrayList;
import java.util.Arrays;
/**
* 使用Java中的泛型方法
*/
public class GenericityMethod1 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("给小花同学的土味情话");
list.add("给小丽同学的土味情话");
list.add("给小路同学的土味情话");
//将list集合转成一个数组并返回
//如果是空参的,那么返回的数组类型为Object类型的.
Object[] objects = list.toArray();
System.out.println(Arrays.toString(objects));
String[] strings = list.toArray(new String[list.size()]);
System.out.println(Arrays.toString(strings));
}
}
4.2 GenericityMethod2
package com.zheng.collection.d3_genericity.genericitymethod;
import java.util.ArrayList;
/**
* 自定义泛型方法
* 定义一个泛型方法,传递一个集合和四个元素,将元素添加到集合中并返回
*/
public class GenericityMethod2 {
public static void main(String[] args) {
ArrayList<String> list1 = addElement(new ArrayList<String>(), "a", "b", "c", "d");
System.out.println(list1);
ArrayList<Integer> list2 = addElement(new ArrayList<Integer>(), 1, 2, 3, 4);
System.out.println(list2);
}
public static <T> ArrayList<T> addElement(ArrayList<T> list , T t1 ,T t2 ,T t3 ,T t4){
list.add(t1);
list.add(t2);
list.add(t3);
list.add(t4);
return list;
}
}
5. 不写泛型的弊端
5.1 GenericityClass
package com.zheng.collection.d3_genericity.genericitysummarize;
import java.util.ArrayList;
public class GenericityClass {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
}
}
5.2 GenericitySummarize
package com.zheng.collection.d3_genericity.genericitysummarize;
import java.util.ArrayList;
import java.util.Iterator;
/**
* 不写泛型的弊端
*/
public class GenericitySummarize {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(123);
Iterator it = list.iterator();
while(it.hasNext()){
String next = (String) it.next();
int len = next.length();
System.out.println(len);
}
}
}
十二、泛型的使用场景,可以作用在什么位置,使用泛型有什么好处
1. 什么是泛型,泛型有什么用,为什么要用
1. 泛型就是一种未知的类,将未知的类型声明在集合、对象上,泛型的默认类型为Object。
例如: ArrayList str = new ArrayList();
这是将String类型指定在str这个集合内,这个集合存储或者读取的类型只能为String类型。
2. 使用泛型可以在传参的时候(使用占位符 ? 占位)不固定传入什么类型,即可通用类型,
如果不设置泛型的上限(例如:< ? extends List>)和下限(例如:<? super List>)。
3. 使用泛型后对象或者集合内只能放入指定的数据类型,可以保证对象或者集合的安全性,减少类型的转换操
作。
2. 怎么使用泛型,泛型可以作用在什么位置
1. 有以下几种使用场景以及放在那种位置
泛型只能定义引用数据类型,而不能使用基本数据类型
泛型类、泛型方法、泛型接口、泛型通配符
例如:作用在类上时( public class Animal { E pet; } ) , 泛型跟在类后边,可以指定用了泛型的类内部的 pet 的类
型。
作用在对象上时( Animal str = new Animal(); ) , 泛型跟在类后边 , 使得对象类的的 pet 属性为 Dog类型。
作用在方法上时( public Animal getPet(){ return E ; } ), 如在类上没有声明泛型时,必须在返回值和访问修饰符
之间声明。
作为方法入参时( public void setPet(E pet){ this.pet = pet ; } ), 如在类上没有声明泛型时,必须在返回值和访问
修饰符之间声明。
3. 使用泛型的好处
1. 使用泛型后对象或者集合内只能放入指定的数据类型,避免出现对象或者集合内的对象在多态使用的时候出现
类型转换异常
(java.lang.ClassCastException),可以保证对象或者集合的安全性。
2. 指定了类型后,对象、集合或方法内只能使用对应的类型,可以减少类型的转换操作(在没有指定类型是类型转
换必须使用
instanceof 关键字来进行判定),缩减了代码了,方便了程序员。
下面以代码的形式讲解
声明在类上时,写在类名后边
package com.jq;
/**
* 声明在类上时,写在类名后边
* @param <E>
*/
class Animal <E> {
/**
* 作为属性声明时,必须在类上声明泛型
*/
E pet;
/**
* 作为参数或者返回值时,如果在类上没有声明,必须在访问修饰符和返回值之间
* @param pet
*/
public void setPet(E pet){
this.pet = pet;
}
public E getPet(){
return pet;
}
public <T> void call(T t){
System.out.println( t.toString() );
}
}
使用泛型实现时,实现类必须使用对应的泛型类。
实现类声明泛型跟接口指定泛型可以同时使用。
package com.jq;
/**
* 作用在接口上,如果实现该接口时指定实现类型
* @param <T>
*/
public interface Animals<T>{
void call(T t);
}
/**
* 拉布拉多犬
*/
/**
* 使用泛型实现时,实现类必须使用对应的泛型类。
* 实现类声明泛型跟接口指定泛型可以同时使用。
*如下代码:
*/
class LaBuLaDuo<T> implements Animals<String> {
public void call(String s) {
System.out.println( s );
}
}
泛型占位符 ? 的使用
如何设置泛型的上限下限
package com.jq;
/**
* 小狗狗
*/
class Dog extends Animal {
public String name;
public Integer age;
}
/**
* 泛型占位符 ? 的使用
* 泛型的上限 < ? extends E> 即传入参数必须为E 的子类
* 泛型的下限 < ? super E>
*/
public class ErHa <E> {
//当参数传递时可以设置上下限 比如下面的是 传入类型必须是 Dog 的子类,上限为Dog
public void call(Animal< ? extends E> s){
System.out.println(s.pet);
}
//传入类型必须是 Dog 的父类,下限为Dog
public void jump(Animal< ? super E> s){
System.out.println(s.pet);
}
// ? 占位符单独使用时,相当于 < ? extends Object >
public void smile(Animal<?> s){
System.out.println(s.pet);
}
public static void main(String[] args) {
new ErHa<Dog>().call(new Dog());
}
}
十三、简述泛型的基本使用和作用,Java面试常见问题解析
前言
在上一篇文章中,给大家讲解了泛型的概念、作用、使用场景,以及泛型集合、泛型接口和泛型类的用法,但受限于篇幅,并没有把泛型
的内容讲解完毕。所以今天我们会继续学习泛型方法、泛型擦除,以及通配符等的内容,希望大家继续做好学习的准备哦。
全文大约【4600】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运
用文中的技术概念,并可以给你带来具有足够启迪的思考...
1. 泛型方法
简介
我们可以在定义接口和类时使用泛型,这样该接口和类中的所有方法及成员变量等处,也都可以使用该泛型。但其实泛型可以应用在整个
类上,也可以只应用在类中的某个方法上。也就是说,方法所在的类可以是泛型类,也可以不是泛型类。方法中是否带有泛型,与其所在
的类有没有泛型没有关系。
泛型方法是在调用方法时才确定类型的方法,泛型可以使得该方法独立于类而产生变化。另外,static静态方法无法访问泛型类的类
型参数,因此,如果想让一个static方法具有泛型能力,就必须使该静态方法成为泛型方法。
语法
我们在定义泛型方法时,需要在方法名的前面添加类型参数。定义泛型方法的语法格式如下:
[访问权限修饰符] [static] [final] <类型参数列表>返回值类型方法名( [形式参数列表])
例如:
public static <T> List showInfoClass<T> clazz, int userId){}
一般情况下,我们编写泛型方法时,必须在该方法的名称前声明要使用的泛型,并且可以同时声明多个泛型,中
间也是用逗号分割。接下来就定义一个泛型方法,给大家具体介绍一下泛型方法的创建和使用。
代码案例
这里我们定义一个泛型方法,用于对数组排序后再遍历元素输出,代码如下:
import java.ut il Arrays;
public class Demo04 {
//定义了一个静态的泛型方法,遍历数组中的每个元素
public static <T> void printArray(T[] arr) {
//先对数组进行排序
Arrays. sort(arr);
//再遍历数组元素
for (T t: arr) {
System. out.print(t +"");
}
System. out printIn();
}
public static void main(Stringt] args) {
Integerl] nums= {100 ,39,8, 200, 65};
//调用泛型方法
printarray(nums);
}
}
在上面的代码中,printArray()就是一个泛型方法,该方法中使用了类型参数T。并且我们在方法的参数中,使用
类型参数T定义了一个泛型数组T[],接着对该数组进行排序和遍历。这样以后无论我们传入任何类型的数组,都可
以在不进行类型转换的前提下,轻松实现排序等功能了,这样我们之前提的需求也就很容易实现了。
2. 通配符
除了以上这些用法之外,泛型中还有一个很重要的通配符功能,接下来我们就来看看它是怎么回事。
简介
泛型中的通配符其实也是一种特殊的泛型类型,也称为通配符类型参数。利用通配符类型参数,可以让我们编写
出更通用的代码,甚至可以在不知道实际类型的情况下使用它们。我们一般是使用 ? 来代替具体的类型参数,例
如 List<?> 在逻辑上可以等同于 List、List 等所有 List<具体类型实参> 的类。
对此,有的小伙伴可能会很好奇,我们为什么需要通配符呢?其实之所以会出现通配符,主要是在开发时,有时
候我们需要一个泛型类型,但我们却不知道该使用哪个具体的类型。在这种情况下,我们就可以使用通配符类型
参数,让代码更加地通用。比如,我们想编写一个可以接受任何类型的集合,并返回其中最大的元素时,此时我
们可能并不确定到底该传入哪个具体的集合,那使用通配符就会更好一些。
通配符的形式
泛型通配符在具体使用时,有如下三种实现形式:
- 未限定通配符(?) : ?表示未知类型的通配符;
- 上限通配符(? extends T) : ?表示类型上限的通配符,T是一个类或接口;
- 下限通配符(? super T) : ?表示类型下限的通配符,T是一个类或接口。
接下来我们针对以上这三种形式,分别通过几个案例来给大家讲解其用法。
未限定通配符(?)
未限定通配符(?)是一种表示未知类型的通配符,它可以在需要一个类型参数的情况下使用。但由于没有限制,因
此它只能用于简单的情况,例如集合中的迭代器或者返回类型是泛型的方法等。
下面是一个简单的例子:
在这个例子中,printElement()方法就接受了一个未知类型的List集合,所以names,ages,numbers都可以作
为这个方法的实参,这就是未限定通配符的作用。
PECS原则
PECS是Producer Extends Consumer Super的缩写,这是关于Java泛型的一种设计原则。该原则表示,如果我
们需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它就是消费者(Consumer),要
使用super通配符。该原则可以指导我们在使用泛型时,该如何定义类型参数的上限和下限。
当我们使用泛型时,可能需要定义类型参数的上限和下限。
例如,我们想要编写一个方法来处理一些集合类型,但我们不知道这些集合中到底有什么类型的元素,此时我们
就可以定义一个类型参数来处理所有的集合类型。一般我们可以利用extends来设置泛型上限,利用super来设置
泛型下限。接下来会在下面的第5和第6小结中,给大家讲解泛型的上限和下限具体该如何实现,请大家继续往下
学习。
上限通配符(? extends T)
上限通配符(?extends T)是一种表示类型上限的通配符,其中T是一个类或接口,泛型类的类型必须实现或继承
T这个接口或类。它指定了可以使用的类型上限,主要是用于限制输入的参数类型。
在这个例子中,printElementUpbound方法中的集合泛型,可以是Number类或其子类,除此之外的其他类型都
不行。也就是说,我们只能使用Number或其子类作为类型参数,泛型类型的上限是Number,这就是上限通配
符的含义。
下限通配符(? super T)
下限通配符(?super T)是一种表示类型下限的通配符,其中T是一个类或接口。它指定了可以使用的类型下限,
主要用于限制输出的参数类型。
下面是一个简单的例子:
在这个例子中,printElementDownbound方法中的集合泛型,可以是Integer或其父类型,即类型下限是
Integer,除此之外的其他类型都不行。也就是说,我们只能使用Integer或其父类作为类型参数,泛型类型的下
限是Integer,这就是下限通配符的含义。
<? extends T>和<? super T>的区别
在这里,要给大家再总结一下<? extends T>和<? super T>
的区别:
- <? extends T> 允许调用 T get()这样的 读方法来获取 T对象 的引用,但不允许调用 set(T)这样的 写方法来传入 T 的引用(传入 null 除外);
- <? super T> 允许调用 set(T)这样的 写方法传入 T对象 的引用,但不允许调用 T get()这样的 读方法来获取 T对象 的引用(获取 Object 除外)。
- 即 <? extends T> 允许读不允许写, <? super T> 允许写不允许读。
大家注意,无论是未限定通配符、上限通配符还是下限通配符,我们既可以在方法中使用,也可以在类或接口中使用。
3. 泛型擦除
我们在学习泛型时,除了要掌握上面这些泛型类、泛型接口、泛型方法以及通配符等内容之外,还要学习泛型擦除的相关内容。那么什么
是泛型擦除呢?我们继续往下学习吧。
简介
所谓的泛型擦除(Type Erasure),就是指在编译时,JVM编译器会将所有的泛型信息都擦除掉,变成原始类型,一
般是将泛型的类型参数替换成具体类型的上限或下限(如果没有指定上界,则默认为Object)。
换句话说,虽然我们在代码中使用了泛型,但在编译后,所有的泛型类型都会被擦除掉,转而使用其对应的原始
类型。这就是Java泛型的底层实现原理。这样设计的目的是为了兼容旧的JDK版本,使得Java具有了较好的向后兼
容性,旧的非泛型代码可以直接使用泛型类库,而不需要进行任何修改。同时,Java也提供了反射机制来操作泛型
类型,使得泛型类型在某些情况下还是可以被获取到的,所以即使有泛型擦除,仍然也不会太影响Java虚拟机的运
行时效率。
比如,在我们定义一个泛型类时,我们会使用泛型类型参数来代替具体的类型,好比下面这个例子:
泛型擦除带来的限制
在编译之后,这个泛型类的类型参数T就会被擦除,成为其对应的原始类型Object。
这也意味着,我们无法在运行时获取到泛型的实际类型参数,所以泛型擦除的使用会有一些限制。首先由于泛型
类型参数被擦除了,因此我们在运行时就无法获得泛型类型参数的信息。例如,如果我们有一个List类型的变量,
在运行时我们就无法获得这个List集合中的元素类型是Integer。另一个限制是在使用泛型类型时,还需要注意类
型安全性。在编译阶段,编译器会检查泛型类型的类型安全性;但在运行阶段,由于泛型类型参数被擦除了,因
此就无法保证类型安全性了。泛型擦除的限制,主要表现在以下几个方面:
无法使用基本类型实例化类型参数;
无法在运行时获取泛型类型信息;
泛型类型参数不能用于静态变量或静态方法;
不能实例化T类型。
接下来再给大家具体分析一下这些限制:
2.1 无法使用基本类型实例化类型参数
Java泛型中的类型参数不能是基本类型,只能是类或接口类型。
例如,以下代码在编译阶段会出错,无法通过编译:
正确的写法是使用基本类型对应的包装类型,如下所示:
2.2 无法在运行时获取泛型类型信息
由于泛型擦除的存在,导致我们在程序运行时无法获取泛型类型的信息。
例如,以下代码在运行时就无法获取List的元素类型:
在输出的结果中,我们只能得到ArrayList的类型信息,而无法获取到集合中具体的泛型类型信息,也就是获取不
到String的信息。但如果我们就是想在运行时获取到泛型的实际类型参数,其实可以参考以下方式进行实现:
在上面的代码中,我们新增了一个方法 getContentType(),该方法用于返回泛型类型参数的实际字节码类型,以
后我们通过调用这个方法,就可以间接地获取到泛型类型的信息了。
2.3 泛型类型参数不能用于静态变量或静态方法
由于泛型类型参数是在实例化对象时才被确定的,因此不能在静态变量或静态方法中使用泛型类型参数。
例如,以下代码是无法编译通过的:
正确的写法是使用一个实际类型来代替泛型类型参数:
2.4 不能实例化T类型
比如在下面的案例中:
上述代码无法通过编译,因为构造方法的两行语句:
擦拭后实际上变成了:
这样一来,创建 new MyClass<String>() 和创建 new MyClass<Integer>() 就变成了Object,编译器会阻止
这种类型不对的代码。如果我们想对泛型T进行实例化,需要借助Class< T >参数并集合反射技术来实现,且在使
用时也必须传入Class< T >。
4. 结语
不过,尽管泛型擦除有一些限制,但泛型仍然不失为一种强大的编程工具,它可以提高代码的可读性和可维护
性。通过合理地使用泛型,我们可以在编译时进行类型检查,避免类型转换的错误和运行时异常,从而提高了代
码的安全性和可靠性。同时,我们也需要了解Java泛型擦除的限制,以便在实际应用中做出正确的决策。
5. 其它
十四、Java泛型详解(史上最全泛型知识详解)
1. 引言
Java中的泛型想必大家都不陌生,我们在创建使用 ArrayList 数组时,IDEA通常会提示我们为这个数组写入泛
型;其实泛型很好理解,我先来简单说明一个场景大家就懂了。假如现在有一个数组,我们需要往里面存入了很
多元素,有String类型,有自定义类型等等;假以时日,我们在需要的时候还会再将它们从容器中取出来,如果把
它们都存放在一个数组中,我们存取的时候,会显得比较混乱。此外还有一个最关键的点,如果我们不使用泛
型,那么我们存入到数组中的对象会被统一当作 Object 类进行处理,在取出的时候,就不能使用我们存入时的类
型特有的方法,要进行强转,这样会非常麻烦。
那么我们能不能对数组进行分类呢?有些数组只存String类型的数据,有些数组只存自定义类型的数据,这样我们
不管是存还是取的时候,都会比较清晰。
既然有了目标,我们再来说方法,怎么样才能对数组进行分类的?
这就要说到我们今天要讲的泛型。
2. 泛型基础篇
本篇主要讲解泛型最基本的定义与用法,刚接触Java的同学可以来简单了解一下泛型到底是怎么回事。
下面我们开始进入正题。
2.1 泛型介绍
泛型是在JDK5之后引入的一个新特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的格式为 <数据类型>
用大白话来说,泛型就好比是给一个标签,通常情况下我们会在开发过程中或者个人学习或练习的过程中使用到
泛型;就拿数组的泛型举例来说,我们把数组比作一个药瓶子,我们药瓶子贴上了什么标签,就放什么药,如果
不管什么药都放在一个药瓶子里,那不得出大事吗?同样容器写上什么泛型,就存放什么数据;这样就不会导致
我们存取数据的混乱。也就解决了我们引言中提到的问题,也就解释了什么是泛型。
2.2 泛型的细节注意点
(1)泛型的数据类型只能填写引用数据类型,基本数据类型不可以。至于原因,我在下面高级篇会提到;
(2)指定泛型的具体类型之后,可以传入该类类型或其子类类型;
(3)如果不手动添加泛型,则默认泛型为 Object 。
2.3 泛型用法简单演示
简单点来说,当我们使用了泛型之后,就好比是给我们要操作的数据贴上了一个标签,你贴上的是什么标签,就
存什么样的数据,否则编译器会报错。如下所示
我们先 new 一个ArrayList数组,然后添加泛型,如果这里填 int 类型,编译器会报错,让你替换为包装类
Integer ,因为int 类型不是引用类型,而它的包装类 Integer 则是引用类型。如果有谁不懂什么是包装类的,或
者有兴趣想要了解的,可以去看我的另一篇文章 “Java中的包装类有什么用?”里面我讲述了八种基本数据类型
对应的包装类以及基本用法。
所以,这里我们需要把 int 改为Integer,改过之后就不会报错了,如下所示:
这个时候我们存入的“123”,“456”,“789”就会被当作Integer对象,那么我来试试存入一个String字符串
会怎么样。
这里编译器告诉了我们几种方法,第一种方法是将字符串“abcdefg”变为Integer类型,但这种方法显然是不行
的;另一种方法就是改变 List 的泛型为 String。
如下,写一个 main 方法,定义了多个 List 对象并标注不同的泛型,添加元素
package cn.itcast.order.pojo;
import java.util.ArrayList;
import java.util.List;
public class FanXing {
public static void main(String[] args) {
// 存放字符串类型数据
List<String> list1 = new ArrayList<>();
list1.add("abcdefg");
list1.add("hijklmn");
System.out.println(list1);
// 存放 Integer 类型数据
List<Integer> list2 = new ArrayList<>();
list2.add(123);
list2.add(456);
list2.add(789);
System.out.println(list2);
// 存放自定义类型数据,提前定义好的Order订单实体类
List<Order> list3 = new ArrayList<>();
list3.add(new Order());
list3.add(new Order());
System.out.println(list3);
try {
System.out.println(Class.forName("cn.itcast.order.pojo.FanXing"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
然后运行main方法,打印容器中的内容
打印输出成功!
所以我们也证实了一个结论,一个容器泛型是什么,就要存入什么类型的数据,否则会报错。
2.4 泛型的好处
通过刚才的简单展示,这里可以简单总结出泛型的几点好处
(1)统一数据类型,对于后续业务层中取出数据有很强的统一规范性,方便对数据的管理;
(2)把运行时期的问题提前到了编译期,避免了强转类型转换可能出现的异常,降低了程序出错的概率;
3. 泛型高级篇
以下就是关于泛型知识的高级篇,可能会有些晦涩难懂,主要以 ArrayList 数组为例,结合ArrayList 源码讲解泛
型的用法,包括泛型能用在哪些地方,几乎解说了泛型所有的用法,如果看不懂可以多看几遍,或者下先掌握基
础篇,以后慢慢提升。
3.1 泛型底层数据存取的实质
刚才我说到了泛型只能用引用数据类型,是有原因的;
当我们往写入泛型的集合中添加元素的时候,泛型其实可以理解成一个看门大爷,你在添加数据之前,它会看看
你要添加的数据类型是否与标注的泛型类型相匹配,不匹配则不会让你存入,匹配的话,在存入之后,容器底层
还是会把你存入的所有数据类型当作 Object 类型保存起来,当你取数据的时候,它会做一个强转,再从 Object
类型强转变成泛型对应的类型。这也就是为什么泛型只能写引用数据类型,因为泛型的底层会做一个强转,在存
取时会在Object类型与泛型类型之间互相强转,显然,int,float,double等基本数据类型是不能强转为Object
类型的,所以泛型必须为引用数据类型,如果想存入 int 类型数据,只能写 int 的包装类 Integer。
3.2 泛型的擦除
通过刚才的讲解,我们知道了,泛型需要定义在容器的后面,并用 <> 进行标注。
其实,Java中的泛型是伪泛型。
为什么要这么说呢?其实我们在编码时期所指定的泛型,只在代码编译时期可以看到,当我们编写的类生成字节
码文件之后,我们加入的泛型 <数据类型> 就会消失,不会在字节码中体现出来,这种现象在Java中有个专业的
名词就叫 “泛型的擦除”。
这里我就不做演示了,感兴趣的小伙伴可以自己试一试,定义一个类,类中添加一个带有泛型的容器,简单写几
个添加打印操作,然后使用编译器编译成字节码文件,查看该字节码文件时你就会发现,我们在编写代码时所写
的泛型其实在字节码文件中并不存在。
3.3 泛型类
泛型的使用方法非常多,这里来简单说一下泛型类的使用;泛型类,就是把泛型定义在类上。
泛型类的使用场景:当一个类中,某个变量的数据不确定时,就可以定义带有泛型的类。
我们平常所用的ArrayList类,就是一个泛型类,我们看如下源码
ArrayList 源码上显示,在ArrayList类的后面,便是 泛型,定义了这样的泛型,就可以让使用者在创建ArrayList
对象时自主定义要存放的数据类型。
这里的 E 可以理解成变量,它不是用来记录数据的,而是记录数据的类型的。可以写成很多字母,T,V,K都可
以,通常这些字母都是英文单词的首字母,V表示 value,K表示 key,E表示 element,T表示 type;如果你
想,自己练习的时候写成ABCDEFG都可以,但建议养成好习惯,用专业名词的首字母,便于理解。
下面我简单自己写一个泛型类,
// 自定义泛型类
public class MyArrayList<T> {
// 给出该数组的默认长度为10
Object[] obj = new Object[10];
// 定义一个指针,默认为0
int size;
// 写一个泛型类中添加元素的方法
public boolean add(T t){
// size默认为0,刚好指向数组的第一个位置,添加元素,将要添加的元素t赋值给到obj数组的第一个位置
obj[size] = t;
// size指针加一,指向下一个位置,下次元素添加到size指向的位置
size++;
// 添加完成并size加一之后,操作完成,返回成功true
return true;
}
// 写一个泛型类中取出元素的方法,index索引可以取出指定位置的元素
public T get(int index){
// 取出元素后,强转为我们泛型所指定的类型
return (T)obj[index];
}
}
定义一个 main 方法,创建我的自定义泛型类的类对象,测试 add 方法,如下所示
这里打印出来的是 list 的内存地址,说明我们自定义的 泛型类没有问题。
其实 ArrayList 底层源码就是这样写的,这里我只是简单的写了两个方法,有兴趣的可以把删除方法和修改方法也
写出来,动手测试一下。
3.4 泛型方法
我们什么时候会用到泛型方法呢?
通常情况下,当一个方法的形参不确定的情况下,我们会使用到泛型方法。
泛型方法其实与泛型类有着紧密的联系,通过上面我写的自定义泛型类不难看出,在泛型类中,所有方法都可以
使用类上定义的泛型。
但是,泛型方法却可以脱离泛型类单独存在,泛型方法上定义的泛型只有本方法上可以使用,其他方法不可用。
泛型方法的格式如下所示
根据上面泛型方法的模板,我们就可以定义一个简单的泛型方法模板
根据模板写一个泛型方法
public class MyArrayList {
private MyArrayList(){}
// 定义一个静态泛型方法,可以封装到工具类中以备后期使用
public static<E> void addAll(ArrayList<E> list, E e1, E e2, E e3){
list.add(e1);
list.add(e2);
list.add(e3);
}
// 写一个main方法测试刚才所写的泛型方法
public static void main(String[] args) {
// 因为泛型方法中需要一个集合对象,所以提前先定义一个集合对象,泛型就写String
ArrayList<String> list = new ArrayList<String>();
// 调用静态方法, 类名.方法名调用
MyArrayList.addAll(list,);
}
}
看下图,当我向泛型为String集合中添加 int 类型的元素时,编译器报错,给出的解决方案是修改定义的addAll方
法,或重新定义一个addAll方法。
这里要知道一点,我们调用了addAll方法,并传入了参数 list ,而list我们定义的泛型为String,所以我们后续添
加的元素类型也只能是String。
因此,当我们传入的参数为String类型的数据时,报错就会消失,打印数组,如下图运行成功!
这里可以总结出一点:泛型方法,在调用它的时候参数类型就已经确定了,该泛型方法会根据给定的参数类型执
行相应的逻辑,得出结果。
泛型方法的使用场景也并不少见,在开发过程中,我们通常会把一些重复或者相似的方法写成一个泛型通用方
法,我们只需要在方法上指定泛型。这样在调用过程中,传入什么样的参数,方法就会执行什么样的逻辑,可以
简化开发,减少代码量,提高编程效率;但对开发者对泛型的理解以及方法的执行逻辑有一定深入的把控与理
解。如果你能写出来,说明你对泛型的理解已经提高了一个层次。
3.5 泛型接口
泛型接口与泛型方法相似,当我们的接口中,参数类型不确定的时候,就可以使用泛型。
泛型接口的格式也很简单,和泛型方法相似,如下图
在Java中,List 接口就是一个泛型接口,我们看源码就可以得知
泛型接口的格式虽然简单,但这不是我们要学习的重点。
我们的重点是:如何使用一个带有泛型的接口?
通常情况下,我们有两种方式
方式一:实现类给出具体的类型。
方式二:实现类延续泛型,在创建对象时再指定泛型类型。
相比于方式一,方式二的扩展性更强。
Java中 List 的实现类 ArrayList 就是采用的第二种方式,延续泛型,我们看源码即可得知
别的不用看,只看我画红线的部分,ArrayList 实现了list接口,但后面还是泛型,延续了泛型,是方式二。
那么我再给各位演示一下方式一,如下我自己定义的一个泛型接口
// 定义一个泛型接口
public interface MyList<E> {
// 定义一个方法做简单测试
public boolean add(E e);
}
再定义一个类实现该接口,
// 定义MyArrayList类实现MyList接口,并在实现时就指定泛型类型
public class MyArrayList implements MyList<String> {
// 定义一个长度为十的默认数组
Object[] object = new Object[10];
// 定义一个size作为指针
int size;
@Override
public boolean add(String s) {
/**
* size初始化为零,刚好指向数组的第一个位置,添加第一个元素时,我们默认将元素添加到数组的第一个位置
*/
object[size] = s;
// size则合理可以作为指针,当添加第一个元素之后,size++,向后移动一位,下一次就会添加到第二个元素的位置,循环往复
size++;
return true;
}
}
可以看到,在实现类中重写add方法,方法的参数就已经确定,就是我们在实现它时指定的String类型。
然后我们写一个main方法测试是否成功
创建对象,添加元素,打印结果,运行发现成功
但这里是一个内存地址,因为我这里只是简单的定义了一个接口,在Java中ArrayList的源码上千行,里面定义了很多方法,我这里只做简
单测试验证一下方式一是如何完成的,很多东西都没有写,大家明白即可。
3.6 泛型的继承和通配符
泛型本身并不具备继承性,但是数据具备继承性。
1. 泛型不具备继承性
如下图,我定义了GrandFathor类,Fathor类,Son类;Fathor类继承GrandFathor类,Son类又继承Fathor类。
我们再定义一个空方法体的 method 方法,方法需要传入一个带泛型的集合,我就写 GrandFathor;
分别创建泛型为 GrandFathor,Fathor,Son 的集合对象 list1,list2,list3;
调用method方法,传入list1,编译器不报错,
传入list2,编译器报错;
传入list3,编译器又报错;
我们可以得出结论,当然了也是事实,泛型是不具备继承性的,也就是说,一个方法传入的对象泛型是什么类型,我们不能把参数泛型的
子类泛型对象作为参数传递给方法,该泛型是不具备继承性的,传入编译器会报错。
2. 何为数据具备继承性
刚才我们验证了也演示了泛型不具备继承性,那么接下来我们来说一下,数据具备继承性是什么意思。
还拿刚才的代码举例,
我们把刚才的代码注释,然后往 list1 对象中添加对象;
添加 GrandFathor 类对象,添加成功,这也是当然的,因为该类的泛型指定的就是 GrandFathor;
添加 Fathor 类对象,发现也添加成功;
添加 Son 类对象,发现也添加成功;
执行 main 方法,如下结果,说明没有问题
如果一个例子不能说明问题,我们再写一个,如下图:
定义一个 list2,还是和刚才一样,运行如下图
这也从侧面说明了一个结论
当我们为一个类指定泛型并创建对象之后,对象中不仅可以加入泛型所指定的类对象,还可以加入泛型类子类的
类对象,这就是数据的继承性。
注意这里说的是对象,上面不具备继承性中说的是参数,不要混为一谈。
3. 泛型的通配符
说回我们刚才3.6.1泛型不具备继承性的例子,method()方法,假设我希望能将GrandFathor类,Fathor类,Son
类的类对象都加入到list集合中去,该怎么做?
很显然,以我们现在的想法和所学的知识,可以给 ArrayList 数组添加一个不确定的泛型,因为不确定类型,所
以 method() 方法中的参数可以是GrandFathor类,Fathor类,Son类的任意类对象,就可以达到我们的目的了。
但各位想过没有,如果传入一个不确定的类型,这样做有没有什么缺点?
其实这样做是有很大一个缺点的,那就是如果添加了这个不确定的泛型,虽然能将GrandFathor类,Fathor类,
Son类的类对象都加入到list集合中去,但其它所有类的类对象也都能加入到该 list,那这还和不使用泛型有什么区
别呢?
继续我们的话题,3.6.1的method()方法,虽然我不确定传入method()方法的类型,但我能确定我要传入的是
GrandFathor类,Fathor类,Son类这三个其中的一个,而且它们三个有继承关系。但是泛型又不具备继承性,我
们又不能直接传入GrandFathor作为泛型,否则另外两个无法作为参数传递进去,那该怎么做呢?
这就要用到我们下面要说的通配符了。
在Java中,泛型的通配符是一个 "?","?" 也代表不确定的类型,它配合关键词 extend 或 super 可以对类型做出
限定。
我们可以写出如下两种写法
- ?extend :这个写法表示可以传递泛型E包括泛型E的所有子类类型。
- ?super :这个写法表示可以传递泛型E包括泛型E的所有父类类型。
根据上面这两种写法,我们就可以对method()方法作出修改,如下图
我们把 method方法中的泛型改为<? extend GrandFathor>,表示可以传入GrandFathor类对象包括其子类对
象,修改之后可以发现,再次调用method方法传入list1,list2,list3,编译器就不报错了。
同理,也可以把泛型改成 <? super Son> 表示Son类以及Son类的所有父类对象,这里就不做演示了,也很简
单。根据上面的例子,我们可以总结出来泛型通配符的使用场景:如果类型不确定,但是知道要传入的参数类型
是某个继承体系中的一个,就可以使用泛型通配符来表示。
十五、泛型在实习项目中的运用
参考链接
1. 项目中的泛型实战
泛型很多都是理论,在项目中怎么用呢?
比如对于常见的缓存穿透,缓存击穿,我们就可以使用泛型将其封装到一个类里面。
比如下面代码,是黑马点评项目中的一个点:通过泛型 + 函数式编程封装成通用解决方案。
难点:
- 泛型方法的使用:返回值类型不确定、id类型不确定。所以就声明泛型,让调用者告诉我们泛型是什么;
- 使用函数式接口:牵扯到数据库查询,需要参数和返回值,使用函数式接口Function<ID,R>
-
- 四大函数式接口 Function<T,R> Predicate Consumer Supplier
/**
* 缓存工具封装
*/
@Slf4j
@Component
public class CacheClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
//缓存击穿使用的线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 将任意java对象序列化为json字符串并存储在string类型的key中,并设置TTL
*
* @param key string类型的key
* @param value 任意java对象
* @param time 时间
* @param unit 单位
*/
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 将任意java对象序列化为json字符串并存储在string类型的key中,并设置逻辑过期时间,用于处理缓存击穿
*
* @param key string类型的key
* @param value 任意java对象
* @param time 逻辑时间
* @param unit 单位
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// RedisData对象,设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 解决缓存穿透
*
* @param keyPrefix key前缀
* @param id id不知道什么类型,所以需要声名泛型ID,名字随意起
* @param type 是什么类型
* @param dbFallback 如果redis查询的不是"",那就需要查询数据库,函数式接口指定逻辑
* @param time 重建缓存后的有效时间
* @param unit 时间单位
* @param <R> 返回值类型,例如Shop类型
* @param <ID> id不知道什么类型,所以需要声名泛型ID,名字随意起
* @return
*/
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是false
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 3.2 如果不存在,则有 null,"",换行 等可能性。如果是"", 则是为了解决缓存穿透而约定的规则
if ("".equals(json)) {
// 解决缓存穿透,不会再去查数据库
return null;
}
// 4.如果不存在,且不是"" ;那么原因可能是缓存中为null,需要根据id去查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis,调用已经写好的方法,超时剔除
this.set(key, r, time, unit);
return r;
}
/**
* 逻辑过期 解决缓存击穿
*/
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type,String lockKeyPrefix, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.1 不存在直接返回null,不是热点key
return null;
}
// 3.2 存在,反序列化为RedisData对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
// 得到R对象
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 4.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 4.1.未过期,直接返回
return r;
}
// 5. 已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = lockKeyPrefix + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock) {
// 6.3.成功,开启独立线程,实现缓存重建
// 在这之前需要DoubleCheck,再次查看redis缓存是否过期
json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isNotBlank(json)) {
// 5.2.2.1 存在则判断是否过期,未过期就直接返回,不需要缓存构建
redisData = JSONUtil.toBean(json, RedisData.class);
r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return r;
}
}
// 6.4 已过期 || 不存在 则重新构建,开启线程池(如果自己new 线程,性能不好)
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存--热点key
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的信息
return r;
}
/**
* 互斥锁 解决缓存击穿
*/
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type,String lockKeyPrefix, long sleepTime,Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是false
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 3.2 如果不存在,则有 null,"",换行 等可能性。如果是"", 则是为了解决缓存穿透而约定的规则
if ("".equals(json)) {
// 解决缓存穿透,不会再去查数据库
return null;
}
// 4.如果不存在,且不是"" ;那么原因可能是缓存中为null,需要根据id去查询数据库
// ==========解决缓存击穿==========
// 4.1 获取互斥锁
String lockKey = lockKeyPrefix + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(sleepTime);
return queryWithMutex(keyPrefix, id ,type,lockKeyPrefix,sleepTime, dbFallback, time, unit);
}
// 4.4 成功,做双重检查锁,查看redis缓存是否存在,存在则无需重建缓存
json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是false
if (StrUtil.isNotBlank(json)) {
// 存在直接返回
r = JSONUtil.toBean(json, type);
return r;
}
// 如果不存在,则有 null,"",换行 等可能性。如果是"", 则是为了解决缓存穿透而约定的规则
if ("".equals(json)) {
// 解决缓存穿透,不会再去查数据库
return null;
}
// 5. 到这里说明通过双重检查锁,代表是第一个线程,则根据id查询数据库
r = dbFallback.apply(id);
// 不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 不要直接返回,因为会自动拆箱,如果为null,会报空指针异常。
// 使用工具类
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}