Hi,小伙伴你好~欢迎进入泛型第三节内容的学习,在学习之前友情提醒一下:学习泛型需要小伙伴们具备一定的javaSE基础,如果之前小伙伴们没有接触过java,大家可以移步到千锋北京java好程序员的javaSE课程进行学习。
没看过以往内容的朋友可以点击博主首页查看哦~
第五关 泛型的离奇失踪
小伙们看到这个标题,可能会大吃一惊,我们不是定义好泛型了吗,那么泛型还会突然离奇失踪啊。
我可以很负责的告诉小伙们: 泛型确实会消失!
1. 泛型的擦除机制(泛型消失)
我们定义的泛型类,泛型方法,只是编译时:规范类型参数的传递,然而在编译成class文件后,运行代码时,这时泛型就消失了。
原因就是:
在JAVA的虚拟机中并不存在泛型,泛型只是为了完善java体系,增加程序员编程的便捷性以及安全性而创建
的一种机制,在JAVA虚拟机中对应泛型的都是确定的类型,在编写泛型代码后,java虚拟机中会把这些泛型参数类型都擦除,用相应的确
定类型来代替,代替的这一动作叫做类型擦除,而用于替代的类型称为原始类型,在类型擦除过程中,一般使用第一个限定的类型来替
换,若无限定,则使用Object.
下面我们测试一下:
//1.在Demo.java类中: 定义一个泛型的集合
List<String> list = new ArrayList<String>();
list.add("hello");
list.add("java");
//2.在编译后的Demo.class文件中,通过反编译查看:泛型消失了
List list = new ArrayList();
list.add("hello");
list.add("java");
2.泛型的擦除补偿
正如我们看到的,我们在代码运行时,泛型就消失了,那么如果我们在运行代码时需要确切的知道泛型的具
体类型该怎么办呢?特别是使用new T(),或者使用instanceof, 因为这两类操作要知道确切的类型。
我们可以通过泛型擦除后的补偿来满足我们的需求,一般我们会采用java中的设计模式来解决这个问题。
-
方式一:简单工厂 (最简单)
在此方法中,将类型作为参数,以一个万能的工厂类(其中有一个返回具体类型类的实例的泛型方法)用类的
newInstance()方法返回参数化类型的实例,如下所示:
/** * 定义泛型T * @param <T> */ public class GenericDemo9<T> { //1.定义泛型变量: t private T t; //2.定义方法:获取t public T getInstance(Class<T> clz){ try { this.t = clz.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return this.t; } //3.测试: public static void main(String[] args) { GenericDemo9<Date> gd = new GenericDemo9<Date>(); Date date = gd.getInstance(Date.class); } }
缺点:
因为class的newInstance()是利用无参构造方法创建对象的,如果该泛型类没有无参构造方法,就会报错
-
方式二:工厂方法(最灵活)
与简单工厂相比,工厂方法更灵活,同时来解决了简单工厂中没有无参构造方法,不能创建对象的问题。
如下所示:
//步骤一: 定义工厂接口 /** * 定义一个泛型接口 * @param <T> */ public interface GenericFactory<T> { T create(); } //步骤二:定义具体创建对象的工厂 /** * 定义生产汽车的工厂 */ class CarFactory implements GenericFactory<Car>{ //1.定义汽车的名称 private String name; //2. 定义汽车对象 private Car car; public CarFactory() { } public CarFactory(String name) { this.name = name; } @Override public Car create() { if(name==null){//没有汽车名称:表示使用无参数构造 this.car = new Car(); }else{//有汽车名称:表示使用有参数构造 this.car = new Car(this.name); } return car; } //3.测试 public static void main(String[] args) { GenericFactory<Car> gf = new CarFactory(); Car car = gf.create();//使用无参构造创建对象 GenericFactory<Car> gf2 = new CarFactory("奔驰S500"); Car car2 = gf2.create();//使用有参构造创建对象 } } //步骤三:定义对象的类 //定义一个汽车类 class Car{ private String name; public Car() { } public Car(String name) { this.name = name; } }
缺点:代码实现起来麻烦
优点:创建对象的方式更加灵活,使用有参和无参构造都能创建对象。
-
模板方法(最简捷)
与工厂方法不同的地方在于:用模板类(抽象类)来控制整个实例化过程的流程,本质就是用模板类控制对象的
创建过程,具体创建对象的实现由模板类的子类去实现,只不过在模板类中需要用工厂方法。
如下所示:
//1.创建模板类 public abstract class GenericTemplate<T> { //1.定义泛型变量 private T t; //2.定义抽象方法 public abstract T create(); } //2.创建模板类的生成者(实现类) class CarCreator extends GenericTemplate<Car> { //1.定义工厂对象:引入工厂方法 private CarFactory cf ; public CarCreator() { this.cf = new CarFactory(); } public CarCreator(String carName) { this.cf = new CarFactory(carName); } //2.重写模板类的方法 @Override public Car create() { return cf.create(); } }
优点:
方式最简捷,因为直接调用具体的生成类即可,我们创建对象时,并看不到模板类的出现。
闯关练习
请描述 代码在运行过程中 泛型在擦除后,具体表示为什么类型?(单选)
A: Class类型
B: T类型
C: Object类型
D: Type类型
答案:C
解析:
在代码运行过程中,泛型会被擦除(也就是泛型会消失),这时泛型的类型通通都会表示为Object类型。
因为定义泛型时,可以指定任意类型,比如List<String>,Set<Number>,所以在泛型擦除后,只有Object类型可以表示任意类型。
第六关 泛型和钻石的“神秘关系”
小伙伴看到钻石是不是特别嗨,学泛型还有钻石呢?非也!在这里这个钻石指的是一种符号,我们开发者又称之为钻石操作符
1. 钻石操作符介绍
钻石操作符是在 java 7 中引入的,钻石操作符其实就是 < > ,可以在里面定义泛型,让代码更易读,但它不能
用于匿名的内部类。
如下所示:
List<Integer> list = new ArrayList<>();// <> 钻石操作符,java 7 以后可以使用
//在java7中: 如果使用匿名内部类,<> 钻石操作符不能直接使用
public class GenericDemo {
public static void main(String[] args) {
Operation<Integer> intOperation = new Operation<Integer>(1) {
@Override
public void handle() {
System.out.println(content);
}
};
intOperation.handle();
}
}
//定义一个抽象类:用来创建匿名内部类
abstract class Operation<T> {
public T content;
public Operation(T content) {
this.content = content;
}
abstract void handle();
}
2.钻石操作符之JDK9新特性
钻石操作符是在 java 7 中引入的,但它不能用于匿名的内部类。 在java 9中可以直接使用
如下所示:
//在java9中: 如果使用匿名内部类,<> 钻石操作符也可以直接使用
public class GenericDemo {
public static void main(String[] args) {
Operation<Integer> intOperation = new Operation<Integer>(1) {
@Override
public void handle() {
System.out.println(content);
}
};
intOperation.handle();
}
}
//定义一个抽象类:用来创建匿名内部类
abstract class Operation<T> {
public T content;
public Operation(T content) {
this.content = content;
}
abstract void handle();
}
第七关 泛型的点点滴滴
走到这里,小伙伴基本上把泛型学完了,是不是很有成就感啊,不要着急走开喔,后面还有些细节需要完善。
下面我们一起把泛型的细节看一下
1.异常中使用泛型
<u>由于泛型在使用过程会消失, 消失后泛型具体的类型就会用户Object类型来替代,Object 是没有 继承 Exception或者Throwable异</u>
<u>常类的, 所以 在定义异常类时,不能使用泛型。</u>
<u>又是因为泛型在使用过程会消失,如果使用泛型来表示异常类型,泛型消失,那么泛型表示的异常就不复存在,就会造成泛型无法捕</u>
<u>获或者处理这种情况。</u>
<u>接下来,小伙们我们一起看如下代码:</u>
//1.自定义带泛型的异常类: 不合法,错误的写法。
class MyEx1<T> extends Exception{
public MyEx1(String message) {
super(message);
}
}
//2.定义泛型方法,抛出泛型T: 不合法
public class GenericDemo {
public <T> T doTest( T t){
try {
System.out.println("测试----");
}catch (T t){//错误的写法
throw t;//错发的写法
}
return null;
}
}
//3.定义泛型接口(类也可以): 合法,正确的写法,但不实用(没有实际用途)。
class GenericTest<E extends Exception>{
void process() throws E;
}
通过代码演示,我们会发现在异常使用泛型存在诸多问题,我们来归纳总结下:
1.自定义带泛型的异常类,不合法。
由于代码在运行时,泛型会被擦除,那么T 会被擦拭成Object类,而Object类显然不会是Throwable的子类,
因此它不符合异常的有关规定,所以java编译器不编译这种错误的写法。
2.定义泛型方法:抛出泛型T,不合法
其实道理和自定义带泛型的异常类的一样,最后T 会被擦拭成Object类,显然不是异常类,无法捕获、亦无法抛出
3.定义泛型接口(类也可以),正确的写法。
因为我们定义了泛型的上限,即使运行时,T会被擦除,那么java编译器依然会把它看成异常,所以不会报错。
但是这种写法,还不如定义非泛型接口或者非泛型类,没有多大实际用途,所以小伙伴知道就可以了。
2.数组与泛型
不能声明参数化类型的数组, 数组可以记住自己的元素类型,不能用普通方法建立一个泛型数组。
(当然 你如果用反射还是可以创建的,用Array.newInstance。因为在反射时,泛型就消失了)
如下所示:
//定义泛型测试类:
class ArrDemo<T>{
public T[] arr1;//标准写法,后期运行时不会引发问题
public ArrDemo<Integer>[] arr2;//不标准写法,后期运行时可能会引发问题
public static void main(String[] args) {
//1.创建对象
ArrDemo<String> ad = new ArrDemo<String>();
//2.操作数组
String[] str_arr1 = ad.arr1;//在创建对象是T指的是String ,arr1就是String类型的数组。
ArrDemo<Integer>[] ad_arr3 = ad.arr2;//ad的泛型是String ,新定义的Integer可能会引发问题
}
}
3.泛型方法
在能够使用泛型方法的时候,尽量避免使整个类泛化。
如下所示:
public class GenericDemo4 {//1.标准: 推荐的写法
//1.定义一个泛型方法:
public <E> E test2(E e){
System.out.println("自定义泛型的方法:"+e);
return e;
}
}
4.泛型类型必须是应用类型,不能是基本类
如下所示:
List<String> list2 = new ArrayList<String>();//正确的写法: 引用类型
List<int> list2 = new ArrayList<int>();//错误的写法: 基本类型
5.虚拟机中没有泛型,只有普通类和普通方法
因为泛型在编译阶段我们能看到,可以规范我们开发者使用的类型。
但是泛型在编译后的class文件,以及最终到 JVM虚拟机中运行,这是泛型被擦除了,所以在虚拟机中只有
普通类和普通方法。
闯关练习
请选择下面描述正确的选项
A: 泛型可以使用基本类型表示
B: 泛型类中必须定义泛型方法
C: 在异常操作中,通常不使用泛型
D: 泛型类型必须是引用类型
答案:C D
第八关 课程总结
这个小节的内容已经学习完了,小伙伴们是不是感觉收获多多!
刚才我们一起了解了泛型的基本信息和使用,同时完成了几个闯关小练习,小伙伴现在对泛型的感觉如何呢。
现在,我们一起做个总结吧。
-
了解泛型是什么 泛型即: "参数化类型",可以把泛型作为"参数", 泛型可以指任意类型,规范java代码的书写
-
了解泛型的优点 泛型可以进行编译期间进行类型检查 泛型可以避免类型强转
-
掌握泛型的定义和使用
定义泛型接口
定义泛型类
定义泛型方法
在实际开发的场景中能够使用上面定义的泛型
-
了解泛型的通配符,以及泛型的上限和下限
泛型的上限,有时又称之为上边界,指的是泛型不能超过extends的类型
泛型的下限,有时又称之为下边界,指的是泛型得高于super的类型
-
了解泛型的擦除机制和补偿措施
泛型在编译后的class文件中就消失了。
泛型在使用时应尽量避免new T(),或者使用instanceof, 因为这两类操作要知道确切的类型。
泛型的补偿措施:简单工厂,工厂方法,模板类,需要小伙们好好学习一下。
-
了解什么是钻石操作符
java7之后,规定< > 尖括号就是钻石操作符,里面的泛型可以省略不写,还是建议不要省略
这就是今天学习到的内容,整体来说,难度呢还是有几分的。
相信你已经掌握了大部分的泛型知识点了。对于还没有理解的知识点。
希望你有时间可以多看看,多敲敲代码,多多练习。
最后希望大家通过这一节泛型的学习,能够对泛型有一个深入的理解,并且能够在实际开发中熟练的运用!
帮助到你的话就点个关注吧~