十三、泛型

一、泛型的理解和好处

在这里插入图片描述

在这里插入图片描述

package com;


import java.util.ArrayList;

/**
 * @author Gao YongHao
 * @version 1.0
 */
public class Generic01 {
    public static void main(String[] args) {
        // 使用传统方法来解决 ===> 使用泛型
        // 解读
        // 1. 当我们 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是Dog类型(细节后面说)
        // 2. 如果编译器发现添加的类型,不满足要求,就会报错
        ArrayList<Dog> dogs = new ArrayList<Dog>();
        dogs.add(new Dog("旺财", 10));
        dogs.add(new Dog("发财", 1));
        dogs.add(new Dog("小黄", 5));
        // 假如不小心添加了一只猫,编译器会报错
//        dogs.add(new Cat("招财猫", 8));
        for (Dog o : dogs) {
            System.out.println(o.getName() + "-" + o.getAge());
        }

    }
}

class Cat {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

在这里插入图片描述

二、泛型的基本介绍

  • 泛型是一种表示数据类型的数据类型。是数据的多态表现。在类声明时使用,可以把数据类型作为参数传入类中(即:参数化类型)
  • 泛型的具体数据类型是在编译期间就确定下来的
    在这里插入图片描述
public class Genric03{
    public static void main(String[] args){
        Person<String> person = new Person<String>("韩顺平教育");
        /*
            可以这要理解,上面的Person类
            
class Person{
    String s; // E 表示 s 的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型
    public Person(String s){ // E 也可以是参数类型
        this.s = s;
    }
    public String f(){ // 返回类型使用E
        return s;
    }
}
        */
    }

}

class Person<E>{
    E s; // E 表示 s 的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型
    public Person(E s){ // E 也可以是参数类型
        this.s = s;
    }
    public E f(){ // 返回类型使用E
        return s;
    }
}

三、泛型基本语法

3.1 语法介绍

在这里插入图片描述

3.2 泛型使用的注意事项和细节

在这里插入图片描述

四、自定义泛型

4.1 自定义泛型类

在这里插入图片描述

package com.generic;

/**
 * @author Gao YongHao
 * @version 1.0
 */
public class Generic02 {
    public static void main(String[] args) {

    }
}

// 解读
// 1. Tiger 后面有泛型,索引我们把 Tiger 成为自定义泛型类
// 2. T,R,M 泛型的标识符,一般是单个大写字母
// 3. 泛型标识符可以有多个
// 4. 普通成员可以使用泛型(属性,方法)

class Tiger<T,R,M>{
    String name;
    T t; // 属性使用泛型
    R r;
    M m;

    // 因为数组在new的使用不能确定T的类型,就无法在内存开空间
//    T[] ts = new T[8];

    // 因为静态是和类相关的,在类加载时,对象还没有创建
    // 所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化
//    static R r2;
//    public static void m1(M m){
//
//    }

    public Tiger(String name, T t, R r, M m) { // 构造器使用泛型
        this.name = name;
        this.t = t;
        this.r = r;
        this.m = m;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public T getT() { // 返回类型可以使用泛型
        return t;
    }

    public void setT(T t) { // 方法使用到泛型
        this.t = t;
    }

    public R getR() {
        return r;
    }

    public void setR(R r) {
        this.r = r;
    }

    public M getM() {
        return m;
    }

    public void setM(M m) {
        this.m = m;
    }
}

4.2 自定义泛型接口

在这里插入图片描述

package com.generic;

/**
 * @author Gao YongHao
 * @version 1.0
 */
public class Generic03 {
    public static void main(String[] args) {

    }
}

/**
 * 泛型接口使用说明
 * 1. 接口中,静态成员也不能使用泛型
 * 2. 泛型接口的类型,在继承接口或者实现接口时确定
 * @param <U>
 * @param <R>
 */
interface IUsb<U, R> {

    // 普通方法中,可以使用接口泛型
    R get(U u);

    void hi(R r);

    void run(R r1, R r2, U u1, U u2);

    // 在 jdk8 中,可以在接口中,使用默认方法,也是可以使用泛型
    default R method(U u) {
        return null;
    }


}

// 实现接口时,直接指定泛型接口的类型
// 给 U 指定 Integer,给 R 指定了 Float
// 所以,当我们实现了IUsb,会使用Integer替换U,使用Float替换 R
class BB implements IUsb<Integer,Float>{
    @Override
    public Float get(Integer integer) {
        return null;
    }

    @Override
    public void hi(Float aFloat) {

    }

    @Override
    public void run(Float r1, Float r2, Integer u1, Integer u2) {

    }

    @Override
    public Float method(Integer integer) {
        return null;
    }
}


// 在继承接口  指定泛型接口的类型
interface IA extends IUsb<String,Double>{
}

// 当我们去实现IA接口时,因为IA在继承IUsu接口时,
// 指定了 U 为 String,R 为Double
class AA implements IA{
    @Override
    public Double get(String s) {
        return null;
    }

    @Override
    public void hi(Double aDouble) {

    }

    @Override
    public void run(Double r1, Double r2, String u1, String u2) {

    }

    @Override
    public Double method(String s) {
        return null;
    }
}

4.3 自定义泛型方法(成员方法 or 静态方法)

  • 注意:泛型方法使用泛型的区别
    在这里插入图片描述
package com.generic;

import java.util.ArrayList;

/**
 * @author Gao YongHao
 * @version 1.0
 */
public class Generic04 {
    public static void main(String[] args) {
        Car car = new Car();
        // 当调用方法时,编译器会根据传入参数,确定泛型的具体类型
        car.fly("宝马", 100);

        // 测试
        // T -> String, R -> Array
        Fish<String, ArrayList> fish = new Fish<>();
        fish.hello(new ArrayList(),11.3f);
    }
}

// 泛型方法,可以定义在普通类中,也可以定义在泛型类中
class Car {// 普通类

    public void run() {
    } // 普通方法

    // 说明
    // 1. <T,R> 就是泛型
    // 2. 是提供给 fly 使用的
    public <T, R> void fly(T t, R r) { // 泛型方法

    }
}

class Fish<T, R> { // 泛型类
    public void run() {

    }

    public <U, M> void eat(U u, M m) { // 泛型方法

    }

    // 说明
    // 1. 下面hi方法不是泛型方法
    // 2. 是 hi 方法使用了类声明的 泛型
    public void hi(T t) {

    }

    // 泛型方法,可以使用类声明的泛型,也可以使用自己声明的泛型
    public <K> void hello(R r, K k) {

    }

}

五、泛型的继承和通配符

在这里插入图片描述

package com.generic;

import java.util.List;

/**
 * @author Gao YongHao
 * @version 1.0
 */
public class Generic05 {
    public static void main(String[] args) {

    }

    public static void printCollection1(List<?> c) {
        for (Object o : c) { // 通配符,取出时,就是Object
            System.out.println(o);
        }

    }

    // ? extends AA 表示 上限;可以接受 AA 或者 AA 子类
    public static void printCollection2(List<? extends AA> c) {
        for (Object o : c) { // 通配符,取出时,就是Object
            System.out.println(o);
        }
    }

    // ? super 子类类名AA:支持AA类以及AA类的父类,不限于直接父类
    public static void printCollection3(List<? super AA> c) {
        for (Object o : c) { // 通配符,取出时,就是Object
            System.out.println(o);
        }
    }

}

六、类型擦除

6.1 类型擦除机制介绍

https://blog.csdn.net/briblue/article/details/76736356 Java 泛型,你了解类型擦除吗?

  • 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容(之前版本借助Object类型与强制转换作为泛型解决方法)
  • 这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
  • 通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
        
System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List<String>List<Integer> 在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
可能同学会问,那么类型 String 和 Integer 怎么办?
答案是 泛型转译

public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }
    
}

Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

打印结果是

erasure class is:com.frank.test.Erasure

Class 的类型仍然是 Erasure 并不是 Erasure<T> 这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型

Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
    System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}

打印结果是

Field name object type:java.lang.Object

那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码

public class Erasure <T extends String>{
//    public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }
    
}

现在再看测试结果

Field name object type:java.lang.String

我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。
所以,在反射中。

public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }
    
    public void add(T object){
        
    }
    
}

add() 这个方法对应的 Method 的签名应该是 Object.class。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
    System.out.println(" method:"+m.toString());
}

打印结果是

method:public void com.frank.test.Erasure.add(java.lang.Object)

也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class) 否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。

6.2 类型擦除带来的局限性

  • 类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性
  • 理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
    在这里插入图片描述

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。

public interface List<E> extends Collection<E>{
    
     boolean add(E e);
}

上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于

boolean add(Object obj);

那么,利用反射,我们绕过编译器去调用 add 方法。

public class ToolTest {


    public static void main(String[] args) {
        List<Integer> ls = new ArrayList<>();
        ls.add(23);
//        ls.add("text");
        try {
            Method method = ls.getClass().getDeclaredMethod("add",Object.class);
            
            
            method.invoke(ls,"test");
            method.invoke(ls,42.9f);
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        for ( Object o: ls){
            System.out.println(o);
        }
    
    }

}

打印结果是:

23
test
42.9

可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。

七、JUnit

在这里插入图片描述

在这里插入图片描述

八、泛型细节

8.1 泛型类或者泛型方法中,不接受 8 种基本数据类型

所以,你没有办法进行这样的编码。

List<int> li = new ArrayList<>();
List<boolean> li = new ArrayList<>();

需要使用它们对应的包装类。

List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();

8.2 对泛型方法的困惑

public <T> T test(T t){
    return null;
}

有的同学可能对于连续的两个 T 感到困惑,其实 <T> 是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 你可以相像一下,如果 test() 这样被调用

test("123");

那么实际上相当于

public String test(String t);

8.3 Java 不能创建具体类型的泛型数组

这句话可能难以理解,代码说明。

List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];

这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List<Integer>List<Boolean> 在 jvm 中等同于 List<Object>,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer> 类型还是 List<Boolean> 类型。
但是

List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];

借助于无限定通配符却可以,前面讲过 ? 代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。

  • 30
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ModelBulider

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值