JAVA进阶-——泛型

目录

前言

一、泛型是什么?

二、为什么我们要使用泛型?

三、泛型的使用

3.1 泛型类

3.2 泛型方法

3.3 泛型接口

四、通配符与上下界

五、泛型在虚拟机中是如何实现的?

总结




前言

接下来一段时间,本人会将近期所看到的学到的一些知识分享在CSDN上,欢迎各位大神前来交流、指教,首先会从JAVA开始,毕竟作为安卓开发工程师,JAVA是基础,后续也会更新一些安卓方面的内容。第一篇博客,是关于JAVA中的泛型。文章中的图表、代码均为原创,如需转载,请注明出处。


一、泛型是什么?

“泛型”的字面意思可以理解为“广泛的类型”、“很多很多的类型”。Java泛型是J2 SE1.5中引入的一个新特性,其本质是“参数化类型”,即将原本具体的类型作为一个可变的参数传递,从而达到“泛化类型”的目的。

二、为什么我们要使用泛型?

一般的类和方法中往往只能使用某种具体的类型,如果想要编写可以应用于多种类型的代码,可能需要重复写多份参数类型不同的代码。泛型的出现主要就是为了解决这种困境。下面是一个简单的例子。

现在我们想要实现一个简单的功能:传入一个数字,将其打印,现在我们希望无论是传入int类型还是double类型的数字,都能够正确将其打印,这时我们可能需要写两个方法,但两个方法的中逻辑相同,只是传入的参数类型和返回值类型不同:


public class MainClass {
    public static void main(String[] args) {
        printInt(1);//打印int型数字
        printDouble(1.2);//打印double型数字
    }

    private static void printDouble(double num) {
        System.out.println(num);
    }

    private static void printInt(int num) {
        System.out.println(num);
    }

}

上述的处理方式暂时能够实现打印int型和double型数字的需求,然而,如果以后还需要实现“打印long型数字”的需求,那我们又得把重复的代码再写一份,只是修改方法中传入和返回的参数类型,这种方式显然是效率低下、不易于维护的。有了泛型之后,我们就可以很轻松解决这个问题:


public class MainClass {
    public static void main(String[] args) {
        print(3L);//使用泛型方法打印long型数字
        print(1);//使用泛型方法打印int型数字
        print(1.2);//使用泛型方法打印double型数字
    }

    private static<T extends Number> void print(T num) {
        System.out.println(num);
    }
}

上面的代码中,我们使用了“泛型方法”,将之前形参中的int、double等类型泛化为T,同样实现了打印数字的功能,而且不用再写大量逻辑相同的代码。这就是泛型的其中一个好处。

泛型还能够给帮助我们避免类型转换错误的问题。List集合相信大家都用过,如果我们不使用泛型,那么任意类型都能够add进集合当中,并且IDE不会提示语法错误,如下:

import java.util.ArrayList;
import java.util.List;

public class MainClass {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add("1");//混进了一个String类型
        for (int i = 0; i < list.size(); i++) {
            int num = (int) list.get(i);
            System.out.println(num * 2);
        }
    }
}

上面的例子中,我们先将集合中的对象强转为int,然后乘以2打印出来,但由于集合中混入了一个String类型数字,显然不能够强转为int,所以运行时就会报错。如果使用了泛型,就能够有效避免这一问题的出现,只要编译时期没有警告,那么运行时期就不会出现类型转换异常

package com.example.libjava;

import java.util.ArrayList;
import java.util.List;

public class MainClass {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add("1");//IDE会直接报错。因为使用了泛型,只允许add进去Integer类型的数据
        for (int i = 0; i < list.size(); i++) {
            Integer num = list.get(i);
            System.out.println(num * 2);
        }
    }
}

上述代码在创建集合的时候就限定了存入元素的类型,什么类型的元素允许存入集合一目了然,增强了代码的可读性

三、泛型的使用

3.1 泛型类

所谓泛型类就是把泛型定义在类上,使用该类的时候,才把类型明确下来,我们可以用如下的方式定义一个泛型类:

package com.example.libjava;


public class GenClass<T> {
    private T data;
    
    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    } 

}

上面就是一个简单的泛型类,泛型类型<T>需要声明在类名之后,此处的T也可以是AA,BB,DB等其他的字符串,语法上并没有太多的限制,但是建议遵循泛型类型的定义标准:通常使用大写字母T、E、K、V等来表示泛型参数,T:Type,E:Element,K:Key,V:value。泛型类上定义的泛型可以在类的普通方法中使用,但不能在静态域或静态方法中使用,这是因为非静态方法需要先有对象,进而在使用泛型的时候早已确定了具体类型。而静态方法不需要创建对象,代码的static部分是先执行的,然后才是构造函数,因此根本没有判断参数类型的约束条件,所以在编译时就报错了。虽然静态方法不能直接使用定义在类上的泛型,但可以采用泛型方法的定义方式,将泛型单独定义在方法上,从而获得使用泛型的能力。

3.2 泛型方法

如果我们只是想在某个方法上使用泛型,则可以只将泛型定义在某个方法上,类中的其他方法无法使用这个泛型,泛型方法中的泛型定义在返回值之前,下面是一个简单的泛型方法的定义:

 public <V> void print(V content) {
        System.out.println(content.toString());
    }

需要说明的是,如果有需要,可以定义多个泛型,比如:

 public <V,C> void print(V value,C content) {
        System.out.println(content.toString());
        System.out.println(value.toString());
    }

3.3 泛型接口

和泛型类类似,泛型同样可以定义在接口上,并被其他类实现,实现类可以选择明确泛型类型,如下所示:

//泛型接口
public interface IGen<T> {
    void print(T t);
}


//实现泛型接口的类
public class Print implements IGen<String> {
    @Override
    public void print(String s) {
        System.out.println(s);
    }
}

实现类也可以不明确泛型类型,而是继续使用泛型,等待其他子类来明确泛型类型:

package com.example.libjava;

public class Print<T> implements IGen<T> {
    @Override
    public void print(T s) {
        System.out.println(s);
    }
}

四、通配符与上下界

这一章开始之前,先看一张动物类的继承关系图,Animal类是他们的顶级父类。

然后再来看一段代码,代码的内容比较简单,打印两个泛型类的data字段。其中print方法的形参要求是一个GenClass<Man>类型的变量,在main方法中,new了两个对象,分别是GenClass<Man>类型和GenClass<YoungMan>类型,既然GenClass<Man>类型的变量能被print方法接受,那么YoungMan作为Man的子类,按说也应该能够被接受才合理,实际上IDE却会报错,提醒Required type和Provided type不一致,也就是说编译器认为GenClass<Man>类型和GenClass<YoungMan>是完全不相同的两个类,不具备任何继承关系,这一现象显然是不太符合面向对象编程思想的。

package com.example.libjava;

public class MainClass {
    public static void main(String[] args) {
        GenClass<Man> manGenClass = new GenClass<>();
        GenClass<YoungMan> youngManGenClass = new GenClass<>();
        print(manGenClass);
        print(youngManGenClass);//报错
    }

    private static void print(GenClass<Man> humanGenClass) {
        System.out.println(humanGenClass.getData());
    }
}

为了解决上面的问题,通配符类型出现了。没有任何限制的通配符类型使用问号?来表示,意味着能够接受任意类型,我们通常称之为“无界通配符”。

有了通配符,我们就可以解决上面所说的问题,我们现在将print方法的形参改为GenClass<?>,意味着GenClass的泛型允许是任意类型,即使我们GenClass的泛型参数使用了Date和Object类型,IDE也不会再报错:

package com.example.libjava;

import java.util.Date;

public class MainClass {
    public static void main(String[] args) {
        GenClass<Animal> animalGenClass = new GenClass<>();
        GenClass<Human> humanGenClass = new GenClass<>();
        GenClass<Man> manGenClass = new GenClass<>();
        GenClass<YoungMan> youngManGenClass = new GenClass<>();
        GenClass<Object> objectGenClass = new GenClass<>();
        GenClass<Date> dateGenClass = new GenClass<>();

        print(manGenClass);//不报错
        print(humanGenClass);//不报错
        print(youngManGenClass);//不报错
        print(animalGenClass);//不报错
        print(objectGenClass);//不报错
        print(dateGenClass);//不报错

    }

    private static void print(GenClass<?> humanGenClass) {
        System.out.println(humanGenClass.getData());
    }
}

但是,实际开发中,往往不需要如此之“泛”的类型,我们通常需要将类型控制在某些范围内,于是我们可以将?配合extends关键字和super关键字使用,从而控制上下界:

  • 无界通配符 < ? >

  • 上界通配符 < ? extends T>

  • 下界通配符 < ? super T>

例如,< ? extends T>表示允许接受的泛型范围是T以及T的子类,其余类型都不能够被接受。如果T取Human,那么<? extends Human>允许的范围如下图红色部分所示:

 

再如,< ? super T>表示允许接受的泛型范围是T以及T的父类,其余类型都不能够被接受。如果T取Human,那么<? super Human>允许的范围如下图红色部分所示:

通过代码,也能很好地验证上述理论,对于print(GenClass<? extends Human> humanGenClass)方法,形参GenClass的泛型传入Animal、Object、Date,都超过了图中红色的范围,因此是不被允许的。

对于print2(GenClass<? super Human> humanGenClass)方法,形参GenClass的泛型传入Man、Date,也超过了图中红色的范围,也是不被允许的。

方法:

package com.example.libjava;

import java.util.Date;

public class MainClass {
    public static void main(String[] args) {
        GenClass<Animal> animalGenClass = new GenClass<>();
        GenClass<Human> humanGenClass = new GenClass<>();
        GenClass<Man> manGenClass = new GenClass<>();
        GenClass<YoungMan> youngManGenClass = new GenClass<>();
        GenClass<Object> objectGenClass = new GenClass<>();
        GenClass<Date> dateGenClass = new GenClass<>();

        print(manGenClass);//不报错
        print(humanGenClass);//不报错
        print(youngManGenClass);//不报错
        print(animalGenClass);//报错
        print(objectGenClass);//报错
        print(dateGenClass);//报错

        print2(manGenClass);//报错
        print2(humanGenClass);//不报错
        print2(youngManGenClass);//报错
        print2(animalGenClass);//不报错
        print2(objectGenClass);//不报错
        print2(dateGenClass);//报错

    }

    private static void print(GenClass<? extends Human> humanGenClass) {
        System.out.println(humanGenClass.getData());
    }

    private static void print2(GenClass<? super Human> humanGenClass) {
        System.out.println(humanGenClass.getData());
    }
}

五、泛型在虚拟机中是如何实现的?

JAVA中的泛型,更像是一种“伪泛型”,它只在程序源码中存在,在编译后的.class文件中,会先将泛型替换为原来的原生类型,然后加入强制转型,以这种方式来实现泛型。这也就意味着,一旦进入运行期,GenClass<Human>、GenClass<Man>、GenClass<OldMan>本质上都是一个类,此时的泛型仿佛被“擦除掉了”,他们都是原生类型GenClass。下图所示的代码中,我们尝试重载print方法,按说两个print方法的形参类型不同的话,是能够重载的,但是IDE出现了报错,报错信息直接翻译是“两个方法具有相同的擦除”,这也就意味着,两个形参GenClass<Man>和GenClass<Woman>在擦除后本质上类型是相同,所以才不允许重载,印证了前面所说的泛型擦除机制。

有了泛型擦除的理论基础,不如实际打开.class文件看一看,看是不是我们说的这样。MainClass源代码如下:

package com.example.libjava;

import java.util.ArrayList;
import java.util.List;

public class MainClass {
    public static void main(String[] args) {
        GenClass<String> stringGenClass=new GenClass<>();
        GenClass<Human> humanGenClass=new GenClass<>();
        GenClass<Man> manGenClass=new GenClass<>();
        GenClass genClass=new GenClass();
        GenClass<Object> genClass2=new GenClass<>();
    }

}

接着我们打开Android studio中存放.class文件的路径,找到MainClass.class,看到的内容如下图所示。

 

查看.class文件内容,我们发现,之前在JAVA文件中尖括号内声明的泛型已经全部不见了,只剩下原生类,再次印证了上文中所说的泛型擦除机制。

总结

上面是我学习JAVA泛型后整理的部分内容,如有错误还请各位大神指出,邮箱hbutys@vip.qq.com

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值