泛型由入门到精通(3)

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,Set,所以在泛型擦除后,只有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.异常中使用泛型

​ 由于泛型在使用过程会消失, 消失后泛型具体的类型就会用户Object类型来替代,Object 是没有 继承 Exception或者Throwable异

常类的, 所以 在定义异常类时,不能使用泛型。

​ 又是因为泛型在使用过程会消失,如果使用泛型来表示异常类型,泛型消失,那么泛型表示的异常就不复存在,就会造成泛型无法捕

获或者处理这种情况。

接下来,小伙们我们一起看如下代码:


//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

第八关 课程总结

这个小节的内容已经学习完了,小伙伴们是不是感觉收获多多!

刚才我们一起了解了泛型的基本信息和使用,同时完成了几个闯关小练习,小伙伴现在对泛型的感觉如何呢。

现在,我们一起做个总结吧。

  1. 了解泛型是什么 泛型即: "参数化类型",可以把泛型作为"参数", 泛型可以指任意类型,规范java代码的书写

  2. 了解泛型的优点 泛型可以进行编译期间进行类型检查 泛型可以避免类型强转

  3. 掌握泛型的定义和使用

    ​ 定义泛型接口

    ​ 定义泛型类

    ​ 定义泛型方法

    ​ 在实际开发的场景中能够使用上面定义的泛型

  4. 了解泛型的通配符,以及泛型的上限和下限

    ​ 泛型的上限,有时又称之为上边界,指的是泛型不能超过extends的类型

    ​ 泛型的下限,有时又称之为下边界,指的是泛型得高于super的类型

  5. 了解泛型的擦除机制和补偿措施

    ​ 泛型在编译后的class文件中就消失了。

    ​ 泛型在使用时应尽量避免new T(),或者使用instanceof, 因为这两类操作要知道确切的类型。

    ​ 泛型的补偿措施:简单工厂,工厂方法,模板类,需要小伙们好好学习一下。

  6. 了解什么是钻石操作符

    ​ java7之后,规定< > 尖括号就是钻石操作符,里面的泛型可以省略不写,还是建议不要省略

这就是今天学习到的内容,整体来说,难度呢还是有几分的。

相信你已经掌握了大部分的泛型知识点了。对于还没有理解的知识点。

希望你有时间可以多看看,多敲敲代码,多多练习。

最后希望大家通过这一节泛型的学习,能够对泛型有一个深入的理解,并且能够在实际开发中熟练的运用!


帮助到你的话就点个关注吧~

你可能感兴趣的:(java,jvm,开发语言)