【泛型】Java中的泛型,泛型类,泛型接口,泛型方法,泛型擦除

一、泛型概念

(1)什么是泛型

什么是泛型???

泛型(Generic),是一种参数化数据类型,它允许我们在编写程序代码的时候不用具体指定需要什么数据类型,而是等到具体使用的时候,将数据类型以参数的形式传递给程序,这就是泛型程序设计。

泛型不是Java中特有的概念,它是一种程序设计思想,在很多编程语言都具有泛型,例如:Golang、Java、Python等等语言都有泛型。

(2)为什么需要泛型

我们来看看下面这个栗子

假设现在有个需求,通过集合保存元素,需要保存String字符串类型和Integer类型,在不考虑泛型的情况下,我们可能会写出下面这样的代码:

String字符串集合

// 字符串集合
class StringBox {
    private String[] box = new String[10];
    private int index = 0;
    public boolean add(String e) {
        if (index < box.length) {
            box[index++] = e;
            return true;
        }
        return false;
    }
}

Integer整数集合

// 整数集合
class IntegerBox {
    private Integer[] box = new Integer[10];
    private int index = 0;
    public boolean add(Integer e) {
        if (index < box.length) {
            box[index++] = e;
            return true;
        }
        return false;
    }
}

从上面两段代码可以看出,我们两个集合类,除了数据类型不同,其余的代码都是类似的。对于这种情况,我们可能会想着,能不能将两个类变成一个类,从而减少重复代码呢???

这时候,我们可能会想到这种方式,我们可以将数据类型用Object替换,这样就可以实现通过一个类保存不同的数据类型了,于是,就写出来下面这样的代码:

// 通用集合
class ObjectBox {
    private Object[] box = new Object[10];
    private int index = 0;
    public boolean add(Object e) {
        if (index < box.length) {
            box[index++] = e;
            return true;
        }
        return false;
    }
    public void print() {
        for (int i = 0; i < index; i++) {
            String str = (String)box[i];
            // 将大写字母转换为小写字母
            System.out.println(str.toLowerCase());
        }
    }
}

这时候,我们写完代码,心里面可能会觉得,我们的代码瞬间提升了一个Level,于是信心满满的在需要使用集合的地方调用我们写的通用集合方法,调用代码如下:

public class WhyUseGeneric {
    public static void main(String[] args) {
        // 使用集合
        ObjectBox objectBox = new ObjectBox();
        objectBox.add(520);
        objectBox.add("LOVE");
        // 打印集合中的元素
        objectBox.print();
    }
}

运行上面的代码之后,居然发现报错了,如下所示:

哎呀,出现数据类型转换失败啦,为什么呢???因为我们的集合中,不仅保存了String类型,还保存了Integer类型,所以在使用的时候,就会发生类型转换失败。

我们应该怎么解决这个问题???

我们可以在使用之前进行类型判断,如果是我们需要的类型,我们就进行强制类型转换,这样在运行过程中,不会发生错误,修改代码如下:

public void printNew() {
    for (int i = 0; i < index; i++) {
        // 判断一下,是不是String类型,不是String类型不处理
        if (box[i] instanceof String) {
            String str = (String)box[i];
            // 将大写字母转换为小写字母
            System.out.println(str.toLowerCase());
        }
    }
}

再次运行程序,这次就可以运行成功了。

通过上面的案例,我们可以发现,如果不使用泛型,我们如果要实现某个功能,针对不同的数据类型,就需要编写很多类似的代码;如果要减少重复代码,可以通过Object类型保存元素,但是需要在使用数据的时候,进行强制类型转换,否则程序报错。

为了解决上面两种情况,于是提出了泛型程序设计:

泛型可以使用同一套代码,实现不同数据类型的操作,并且在程序运行过程中,不需要我们进行强制类型转换。

本篇文章后面,我会介绍泛型的具体使用,请继续往后看。

二、泛型的优缺点

(1)优点

  • 减少重复代码,提高代码的复用性。
  • 可以不用强制类型转换。
  • 提供了编译期间的类型安全检查机制。

(2)缺点

  • 泛型的数据类型不能是基本数据类型。
  • 方法重载中不能使用相同参数列表的泛型。
  • 泛型的类型检查是编译期间的,通过反射机制可以绕过类型检查机制。

三、泛型的使用

(1)泛型类

泛型使用在一个类上面,这个类就称为:泛型类

如何定义一个泛型类???

// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
}

语法格式:

访问修饰符   类名称<T> {}

  • 类名称后面使用左右尖括号【<>】标识
  • 左右尖括号【<>】里面,使用一个泛型标识,标识可以是任意字符,一般常用的是:T、K、V、E等。

泛型类定义之后,我们就可以在类里面使用这个泛型。下面给出一个案例代码:

// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
    // 定义泛型变量
    private T data;
    // 定义方法
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return this.data;
    }
}

泛型类的使用

public class GenericDemo {
    public static void main(String[] args) {
        // 泛型类的使用
        // 保存String类型
        GenericClassDemo<String> generic = new GenericClassDemo<>();
        generic.setData("泛型类的使用");
        // 输出结果
        System.out.println(generic.getData());
        
        // 保存Integer类型
        GenericClassDemo<Integer> generic2 = new GenericClassDemo<>();
        generic2.setData(520);
        System.out.println(generic2.getData());
    }
}

看完泛型类的使用后,是不是觉得代码写的很丝滑呢???即可以减少重复代码,又可以避免强制类型转换。

泛型类的继承使用

如果一个类,继承了泛型类,那么子类有两种处理泛型的方式:

  • 第一种方式:子类给父类中的泛型参数指定具体的数据类型。
  • 第二种方式:子类也定义为泛型类。
  • 第一种方式:子类给父类中的泛型参数指定具体的数据类型

我们在定义子类的时候,可以给父类传递具体的泛型类型,告诉父类泛型应该使用哪个具体的数据类型。

// 子类给父类指定具体的数据类型
public class GenericExtendsDemo extends GenericClassDemo<String> {
}
class GenericClassDemo<T> {
    private T data;
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return this.data;
    }
}
  • 第二种方式:子类也定义为泛型类

如果子类没有指定父类的数据类型,那么子类就需要声明为泛型类(注意:子类的泛型参数标识符必须和父类的泛型参数标识符一致,否则编译报错)。

// 子类也定义为泛型类
public class GenericExtendsDemo<T> extends GenericClassDemo<T> {
}
class GenericClassDemo<T> {
    private T data;
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return this.data;
    }
}

 如果子类不是泛型类,那么父类也不能使用指定【<T>】修饰,可以不写,不写就默认是【Object】数据类型。

(2)泛型接口

和泛型类基本类似,只不过是定义在接口上面。

泛型接口:泛型声明在接口上面,就叫做泛型接口。

案例代码如下所示(很简单,看一眼就会了):

// 定义泛型接口
public interface GenericInterfaceDemo<E> {
    // 定义方法
    void setData(E data);
    E getData();
}

泛型接口的实现

和泛型类的继承是类似的,也有两种方式:

  • 第一种方式:实现类指定具体的数据类型。
  • 第二种方式:实现类定义为泛型类(注意:实现类的泛型参数标识符必须和接口的泛型参数标识符一致,否则编译报错)。

下面给出两种方式的演示代码,大致看看就可以啦,非常简单滴。

  • 第一种方式:实现类指定具体的数据类型
// 泛型实现类指定具体的数据类型
public class SubGenericInterface02 implements GenericInterfaceDemo<String> {
    @Override
    public void setData(String data) {
        
    }
    @Override
    public String getData() {
        return null;
    }
}
  • 第二种方式:实现类定义为泛型类
// 泛型实现类
public class SubGenericInterface<T> implements GenericInterfaceDemo<T> {
    @Override
    public void setData(T data) {
 
    }
    @Override
    public T getData() {
        return null;
    }
}

以上就是泛型接口的定义和使用。

(3)泛型方法

在前面,我们定义泛型类的时候,我们可以发现一个问题,就是我们在类中定义了一个包含泛型参数标识【T】的方法,如下所示:

// 定义一个泛型类
// 语法格式:类名称<T>
public class GenericClassDemo<T> {
    // 定义泛型变量
    private T data;
    // 定义方法
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return this.data;
    }
}

很多人可能会认为上面的【setData()、getData()】就是泛型方法,其实严格来说那个不是泛型方法,那只是类中一个使用到了泛型参数的普通方法而已。

那什么是泛型方法呢???

什么是泛型方法???

泛型方法是在方法调用的时候,将泛型的数据类型作为参数传递给方法,而不是通过泛型类进行参数传递。

换句话说,泛型方法不需要依赖泛型类,它可以单独在一个普通的类中使用泛型参数。

通过泛型方法的概念,我们就可以知道,泛型方法是可以定义在普通的类里面的,它只需要在调用方法的时候进行参数传递,而不需要借助泛型类进行参数传递。

如何定义一个泛型方法呢???

泛型方法定义语法格式:

修饰符   <T>   返回值类型   方法名称(T data,......) {}

  • 【<T>】在方法的访问修饰符和返回值之间,采用【<T>】标识这个方法是泛型方法,它的参数类型在调用时候决定。
  • 【T data】方法参数,在调用方法的时候传递。

上面就是泛型方法的定义格式,我们来看下代码案例:

public class GenericMethodDemo {
    public static void main(String[] args) {
        // 泛型方法调用
        GenericMethod generic = new GenericMethod();
        // 调用
        generic.method(520);
        // 带有返回值的泛型方法
        String ret = generic.method02("面向对象编程");
        System.out.println("返回值:" + ret);
 
        // 调用泛型静态方法
        String staticRet = GenericMethod.method03("Static Generic Method.");
        System.out.println(staticRet);
    }
}
class GenericMethod {
    // 定义泛型方法
    public <T> void method(T data) {
        System.out.println("这是定义的一个泛型方法......");
        System.out.println("方法参数值: "+data);
    }
    // 返回值也是泛型的泛型方法
    public <T> T method02(T data) {
        System.out.println("这是定义的一个具有泛型返回值的泛型方法......");
        return data;
    }
    /** 泛型静态方法 */
    public static <T> T method03(T data) {
        System.out.println("这是泛型静态方法......");
        return data;
    }
}

程序运行结果如下所示:

静态方法使用泛型的要求

  • 如果一个静态方法使用了泛型参数,那么这个静态方法必须声明为泛型方法,否则编译不通过。
class GenericStaticMethod<T> {
    // 普通方法中使用泛型参数
    public void method(T data) {
        System.out.println("普通方法中,使用泛型参数......");
    }
    // 静态方法中使用泛型参数
    public static void method02(T data) {
        // 编译不通过,必须声明静态的泛型方法
    }
    public static <T> void method03(T data) {
        // 静态的泛型方法
    }
}

静态方法如果不定义为泛型方法,并且还使用泛型类中的泛型参数,则会编译不通过。

注意:静态方法,是不能访问【泛型类】中的【泛型参数标识符】。

泛型类和泛型方法同时出现的情况

  • 当我们在使用泛型类的时候,如果还需要在类中定义泛型方法,那该怎么办呢???

解决办法:

  • 可以使用不同的泛型标识符,比如:泛型类中使用【T】,而泛型方法中则使用【E】。

其实泛型方法依旧可以使用和泛型类一样的泛型参数标识符【T】,只不过为了区分一下,一般情况下,我们都尽量不使用相同的泛型标识符。

案例代码如下所示:

// 定义泛型类
class GenericClassAndMethod<T> {
    // 定义泛型方法
    public <T> void method(T data) {
        System.out.println("这是泛型方法......");
        System.out.println("参数类型:" + data.getClass());
    }
    // 使用不同的泛型标识符
    public <E> void method02(E data) {
        System.out.println("这是泛型方法......");
        System.out.println("参数类型:" + data.getClass());
    }
}

以上,就是泛型方法的定义和使用,是不是So easy~~~。

(4)泛型通配符

泛型通配符

  • Java中通过使用【?】来表示泛型通配符,含义是:未知的类型。
  • 【?】泛型通配符是一个无界的泛型,就是可以为任意的数据类型。

这里我个人是不太理解的,泛型都已经是参数化类型,那按理来说,泛型就应该是未知的类型,可以传入任意的数据类型,为什么还需要额外定义一个泛型通配符呢???

(5)泛型上界

泛型的上界:

  • 泛型的上界,是指规定传入的数据类型只能是某个具体数据类型及其子类型。

泛型上界定义格式:

上界语法格式:

  • <T extends XXX>
  • T:表示泛型参数
  • XXX:表示具体的数据类型,比如:String、Integer、等等。

下面给出一个泛型上界的案例代码:

public class GenericExample {
    public static void main(String[] args) {
        // 使用泛型上界
        GenericExtends<Integer> generic = new GenericExtends<>();
        // 如果传递一个不是Number的子类,则编译不通过
        GenericExtends<String> generic2 = new GenericExtends<String>();
    }
}
// 定义一个泛型上界的泛型类
class GenericExtends<T extends Number> {
    
}

上面的代码会出现编译不通过,因为在使用的时候,传入的数据类型不在泛型上界范围里面。

上面编译错误的大致意思是:类型参数String不在范围里面,应该要继承Number类型的。

(6)泛型下界

既然可以限制泛型的上界,当然也可以限制泛型的下界啦。

泛型下界:

  • 泛型的下界,是指规定传入的数据类型只能是某个具体数据类型及其父类型。

泛型上界语法格式:

语法格式:

  • <XXX super T>
  • XXX:是指具体的数据类型,比如:String、Integer、等等。
  • T:是指泛型参数。

案例代码如下所示:

class GenericSupers {
    // 泛型下界
    public void method(List<? super Integer> list) {
        
    }
}
我发现,泛型下界的不能定义在类或者接口上面,只能用在方法上面。

(7)泛型擦除

什么是泛型擦除???

泛型是只能在编译期间生效的,到了程序运行期间的时候,程序不认识那些泛型参数,所以在编译期间,泛型的那些参数都会被擦除,也就是被替换为具体的数据类型。

  • 如果没有指定泛型的上下界,那么默认情况下,所有的泛型参数,在泛型擦除后将会是Object类型。
  • 如果指定上界类型,那么泛型擦除之后,数据类型将和泛型上界的数据类型相同。
    • 举个栗子:List<? extends String>,这个泛型擦除之后,数据类型将变成String类型。
  • 如果指定下界类型,那么泛型擦除之后,数据类型将和泛型下界的数据类型相同。
    • 举个栗子:List<? super Student>,这个泛型擦除之后,数据类型将变成Student类型。
  • 默认情况下,所有泛型参数,在泛型擦除之后都会变成Object类型
public class GenericEraseDemo {
    public static void main(String[] args) {
        // 创建String类型
        GenericEraseClass<String> stringGeneric = new GenericEraseClass<>();
        // 创建Integer类型
        GenericEraseClass<Integer> integerGeneric = new GenericEraseClass<>();
        // 输出两个数据类型
        Class stringClazz = stringGeneric.getClass();
        System.out.println(stringClazz);
        Class integerClazz = integerGeneric.getClass();
        System.out.println(integerClazz);
 
        System.out.println("===============================");
 
        // 利用反射查看两个类中的参数data
        Field[] fields = stringClazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("GenericEraseClass<String>中data的参数类型: "+field.getType());
        }
        fields = integerClazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("GenericEraseClass<Integer>中data参数类型: "+field.getType());
        }
    }
}
class GenericEraseClass<T> {
    private T data; // 定义一个泛型变量
}
查看上面代码运行结果,发现两个不太类型的泛型参数,居然都输出了相同的数据类型,都是【Object】类型。

  • 泛型上界,在泛型擦除之后会变成和上界相同的数据类型

public class GenericEraseDemo {
    public static void main(String[] args) {
        // 创建String类型
        GenericEraseClass<Long> stringGeneric = new GenericEraseClass<>();
        // 创建Integer类型
        GenericEraseClass<Integer> integerGeneric = new GenericEraseClass<>();
        // 输出两个数据类型
        Class stringClazz = stringGeneric.getClass();
        Class integerClazz = integerGeneric.getClass();
 
        // 利用反射查看两个类中的参数data
        Field[] fields = stringClazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("GenericEraseClass<Long>中data的参数类型: "+field.getType());
        }
        fields = integerClazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("GenericEraseClass<Integer>中data参数类型: "+field.getType());
        }
    }
}
// 定义泛型上界
class GenericEraseClass<T extends Number> {
    private T data; // 定义一个泛型变量
}
上面定义了一个以【Number】类型的泛型上界,运行结果后,发现泛型擦除之后,对应的变量会变成【Number】的数据类型。

  • 泛型下界,在泛型擦除之后会变成和下界相同的数据类型

这里通过反射机制,我不知道怎么查看擦除后的类型,所以这里我通过查看字节码的方式,来查看泛型擦除之后的类型。

查看字节码文件内容的命令:

  • javap -verbose XXXX.class

 从字节码文件里面,可以看出,泛型擦除之后,方法的参数类型变成和泛型下界的数据类型相同,都是Integer类型。

(8)泛型擦除带来的问题

  • 泛型擦除是Jdk1.5之后出现的新特性,但是为了让之前的Jdk版本也可以运行泛型程序,所以就提出了泛型擦除,将其数据类型全变成了Java中的原始类型,从而实现兼容性。(因为如果不进行擦除,那么Jdk就需要额外添加处理泛型参数的代码,这样就导致Jdk1.5之前的版本,无法识别泛型参数了,因此也就不能兼容Jdk1.5之前的版本,所以泛型擦除就出来了
  • 泛型擦除只能在编译期间,对数据类型进行限制,但是通过反射机制,可以绕过泛型的限制。
  • 泛型参数不能是基本数据类型,因为擦除之后会变成Object类型,而基本数据类型不能转换成Object,因为泛型参数只能是引用数据类型。
  • 泛型类中的静态方法和属性不能使用泛型类中的参数类型(因为静态方法加载在类实例化之前)。
  • catch代码块中,不能使用泛型(因为两个catch中使用相同的异常类型,但是泛型参数不同,那么泛型擦除后,就会导致两个catch块一致,从而编译错误)。

(9)泛型数组

  • Java中不支持直接通过new创建一个泛型的对象或者数组(编译会报错)。

那如果要创建泛型数组呢???

有下面几种方式,创建泛型数组的方式:

  • new后面不指定泛型参数。
  • 通过java.lang.reflect.Array类中的newInstance()方法创建泛型数组。

创建泛型数组

代码如下所示:

public class GenericDemo<T> {
    private T[] data;
 
    // 方式一:new后面不加泛型类型<T>
    private List<T>[] list = new LinkedList[10];
 
    // 方式二:通过Array类的newInstance()方法
    public GenericDemo(Class<T> clazz, int length) {
        data = (T[]) Array.newInstance(clazz, length);
    }
}

以上,就是Java中泛型相关的知识点,如果有哪里写的不正确的地方,希望大家纠正一下。

这个博客也可以:java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一_VieLei的博客-CSDN博客_泛型

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值