Java泛型

1、为什么需要泛型?

1.1可以为多种数据类型执行相同的代码

public class NonGeneric {
    
    public int addInt(int x,int y){
        return x + y;
    }

    public float addFloat(float x,float y){
        return x + y;
    }

    public static void main(String[] args) {
        NonGeneric nonGeneric = new NonGeneric();
        System.out.println(nonGeneric.addInt(1,2));
        System.out.println(nonGeneric.addFloat(1f,2f));
    }
    
}

我们可以看到上面这段代码,int类型,float类型都有各自的加法方法,如果我们还要增加double类型的话,我们还要再加一个double的加法方法,这样显然是麻烦的。

1.2可以在编译期间指定数据类型,不需要强制类型转换,以及插入错误的数据类型在编译期间就能发现。

public class NonGeneric {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("String");
        list.add(100);
        int size = list.size();
        for (int i = 0; i < size; i++) {
            String name = (String) list.get(i);
            System.out.println("name:" + name);
        }
    }
}

运行后报错
image.png

2、泛型类、泛型接口和泛型方法

2.1泛型方法

完全独立的,不一定非要定义在泛型类或泛型接口中,在普通类中同样可以定义。
在访问修饰符和返回值之间声明<T>,这种方法就是泛型方法,这是必要的,否则不是泛型方法。

public static <T> T genericMethod(T... a){
        return a[0];
    }

2.2泛型类和泛型接口的定义

//泛型类
public class NormalGeneric<T> {
    private T data; 
    
    //该方法不是泛型方法 只不过返回值是泛型T 只有加入<T>的方法才是泛型方法
    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
//泛型接口
public interface IGenertor<T> {
    T next();
}
//泛型接口实现类1 不指明确切类型
public class ImpGenertor<T> implements IGenertor<T> {
    @Override
    public T next() {
        return null;
    }
}
//泛型接口实现类2 明确具体类型
public class ImpGenertor2 implements IGenertor<String> {
    @Override
    public String next() {
        return null;
    }
}

泛型方法和泛型类或泛型接口的区别
泛型类或泛型接口在创建实例对象时传入具体类型,泛型方法在调用时传入(不传入也可以,编译器会自动识别)

public class NonGeneric {

    public static <T> T genericMethod(T... a){
        return a[0];
    }

    public static void main(String[] args) {
        //泛型类
        ImpGenertor<String> impGenertor = new ImpGenertor();
        //泛型接口 自动识别类型
        System.out.println(genericMethod("111","2222"));
    }
}

泛型方法和泛型类或泛型接口 可以有多个泛型
例如:

public interface IGenertor<T,K> {
    T next();
    K firrs();
}

下面我们看一段代码

public class NonGeneric {
    static class Fruit{
        @Override
        public String toString() {
            return "水果";
        }
    }

    static class Apple extends Fruit{
        @Override
        public String toString() {
            return "苹果";
        }
    }

    static class Person{
        @Override
        public String toString() {
            return "人";
        }
    }

    static class GenericTest<T> {

        public void show_1(T t) {
            System.out.println(t.toString());
        }
        
        public <E> void show_2(E e) {
            System.out.println(e.toString());
        }

        public <T> void show_3(T t) {
            System.out.println(t.toString());
        }

    }

    public static void main(String[] args) {
        Fruit fruit = new Fruit();
        Apple apple = new Apple();
        Person person = new Person();

        GenericTest<Fruit> genericTest = new GenericTest<>();

        genericTest.show_1(fruit);
        genericTest.show_1(apple);
        //编译器会报错
//        genericTest.show_1(person);

        genericTest.show_2(apple);
        genericTest.show_2(fruit);
        genericTest.show_2(person);

        genericTest.show_3(apple);
        genericTest.show_3(fruit);
        genericTest.show_3(person);
    }
}

由上面的代码可知:
1、不是声明在泛型类里面的就是泛型方法
2、在泛型类中使用泛型方法,也要在访问修饰符和返回值之间将泛型<T>声明出来
3、泛型方法声明的泛型是独立的,所以show_2可以传入任意类型,不和泛型类中定义的泛型有任何关系
4、如果泛型方法声明的泛型和泛型类中声明的泛型名称一致,也遵循上一条,所以show_3使用上与show_2没有任何区别
5、泛型类声明的泛型只影响泛型类中使用该泛型的部分,不会影响独立的泛型方法。

3、如何限定类型变量

public static <T> T min(T a,T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

上面这段代码编译器会报错,因为编译器不能确定T类型一定含有compareTo方法,那么我们如何保证执行泛型这个类型变量一定有某个方法或遵循某种规则呢?
我们如果在传入参数时,限制只有实现compareTo方法才可以传入是不是就解决了这个问题,这个思想就是类型变量的限定。
我们将代码改为下面这种形式

public static <T extends Comparable> T min(T a,T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

这样就没问题了,限制T必须实现或者派生自Comparable这个接口
T表示应该绑定类型的子类型
Comparable表示绑定类型也就是限定类型
T和Comparable 可以是类 也可以是接口
限定类型可以是多个,用&连接,但是要注意,如果限定类型有接口,也有类,类必须要写在限定类型的第一个,而且必须是有且仅有一个类。

//这么写会报错
public static <T extends Comparable& ArrayList> T min(T a, T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }
//这么写没问题
  public static <T extends ArrayList&Comparable> T min(T a, T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

为什么只能有一个类?
因为Java是单继承的
为什么一定要写在第一个?
问Java去吧 我也不知道
注意:上面写的示例是泛型方法,但是规则对泛型类和泛型接口同样适用。

4、泛型使用中的约束和局限性

1、不能实例化类型变量
2、静态域或者静态方法里不能使用,需要注意的是静态方法本身是泛型方法就可以使用
3、基本数据类型不可以,可以传入基础类型的包装类,为什么不可以,因为T只能是对象,基础数据类型不是对象
4、不能使用instanceof关键字 判断具体类型
5、泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
6、可以定义泛型数组 但是不能初始化
7、泛型类不能extends Exception/Throwble
8、不能捕获泛型类对象

public class Restrict<T> {
    private T data;

    //1、不能实例化类型变量
//    public Restrict() {
//        this.data = new T();
//    }

    //2、静态域或者静态方法里不能使用
//    private static T instance;
    //静态方法本身是泛型方法就可以使用
//    private static <T> T getInstance(){
//        T a = null;
//        return a;
//    };

    public static void main(String[] args) {
        //3、基本数据类型不可以会报错
//        Restrict<double>

        Restrict<Double> restrict = new Restrict<>();
        //4、不能使用instanceof关键字 判断具体类型 这里会报错
//        if(restrict instanceof Restrict<Double>)

        //5、泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
        Restrict<String> restrictStr = new Restrict<>();
        //这里打印结果为true
        System.out.println(restrict.getClass() == restrictStr.getClass());
        //下面打印的名字是一样的  Restrict类名  泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
        System.out.println(restrict.getClass().getName());
        System.out.println(restrictStr.getClass().getName());

        //打印结果
//        true
//        com.hot.lib.day1.Restrict
//        com.hot.lib.day1.Restrict

        //6可以定义泛型数组 但是不能初始化
        Restrict<Double>[] restrictArray;
        Restrict<Double>[] restricts = new Restrict<Double>[10];

    }

    //7、泛型类不能extends Exception/Throwble 这里编译器会报错
//    private class Problem<T> extends Exception{}

    //8、不能捕获泛型类对象
//    public <T extends Throwable> void dowork(T x){
//        try {
//
//        }catch (T t){
//
//        }
//    }
    
    //这种方式可以
    public <T extends Throwable> void dowork(T x) throws T{
        try {

        }catch (Throwable t){
            throw x;
        }
    }
}

5、泛型类型的继承规则

这里我们用代码来提现

class Employee {
}

class Worker extends Employee {
}

public class Pair<T> {

    public static void main(String[] args) {
        Pair<Worker> workerPair = new Pair<>();
        Pair<Employee> employeePair = new Pair<>();

        //我们平时这么写是没问题的  因为Worker extends Employee
        Employee employee = new Worker();

        //1、这里编译器会报错  workerPair和employeePair 不存在继承关系
//        Pair<Employee> employeePair1 = new Pair<Worker>();

        //这里是没问题的
          Pair<Employee> pair = new ExtendPair<>();
        
    }

    //2、泛型类可以继承或者扩展其他泛型类 比如List和ArrayList
    private static class ExtendPair<T> extends Pair<T>{}
}

6、泛型中的通配符类型

只能用在方法中,类中是不可以使用的
这里比较复杂,首先我们先看几个测试类。

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

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

public class Food {
}

public class Fruit extends Food{
}

public class Apple extends Fruit{
}

public class Orange extends Fruit{
}

public class Tomatoes extends Apple {
}

注意上面这些类的继承关系。

我们首先看第一种情况

 public static void println(GenericType<Fruit> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        println(fruitGenericType);
        //编译器报错
//        println(appleGenericType);
    }

这种情况很简单,我们上面说过,泛型参数之间存在继承关系,但是泛型类没有任何关系,所以这里传入GenericType<Apple>类型会报错。正是因为如此,才引入了通配符的概念。

接下来我们看下面这段代码

public static void println(GenericType<? extends Fruit> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        GenericType<Food> foodGenericType = new GenericType<>();
        println(fruitGenericType);
        //这时可以编译通过
        println(appleGenericType);
        //编译报错
//        println(foodGenericType);

        GenericType<? extends Fruit> genericType = new GenericType<>();
        //不会报错
        Fruit fruit = genericType.getData();
        //下面两行代码都会报错
//        genericType.setData(new Apple());
//        genericType.setData(new Fruit());
    }

这里我们可以看到打印方法的泛型改为? extends Fruit,?就是通配符,extends则是我们上面讲的限定符(上界限定符),这里的意思是,限定泛型类型为Fruit的派生类,包括Fruit本身。所以我们看到,println传入Fruit的子类型都没问题了,但是传入Food就会报错,因为Food是Fruit的父类。

接下来我们看到,我们创建了一个genericType对象,然后分别调用了getData和setData方法。
getData方法返回的是Fruit类型,因为我们知道传入的上限就是Fruit类型,如果传入的是Fruit派生类型,也是可以转为Fruit类型,所以这里会返回Fruit类型。

然后我们发现,setData方法会报错,因为我们没有指定确切的类型,虽然我们传入的是Fruit的子类,但是编译器不知道是哪个子类,所以会报错。

extends限定符的作用就是可以安全的访问数据,也就是extends后面的类型数据,即例子中的Fruit类型数据。

接下来我们看另外一种情况

public static void println(GenericType<? super Apple> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        GenericType<Orange> orangeGenericType = new GenericType<>();
        GenericType<Tomatoes> tomatoesGenericType = new GenericType<>();
        println(fruitGenericType);
        println(appleGenericType);
        //下面两行编译报错
//        println(orangeGenericType);
//        println(Tomatoes);

        GenericType<? super Apple> genericType = new GenericType<>();
        //不会报错
        Object fruit = genericType.getData();
        //不会报错
        genericType.setData(new Apple());
        //编译报错
//        genericType.setData(new Fruit());
    }

这种情况与上面的区别就是将extends换成了super,super是下界限定符,即只能是Apple的父类及其自身。所以我们看到只能传入父类型,传入子类型或平级的类型会报错。

这时调用getData方法,会返回Object类型,不会返回具体类型,这很好理解,因为我们限制了只能传入Apple的父类型,但是我们并不知道具体是哪个父类型,因为Object是所有类的基类,所以这里返回了Object。

然后我们看setData调用,这里的逻辑可能会有些让人不舒服,我们可以看到传入Apple的父类型,也就是Fruit类型会报错,大家一定会疑惑这是为什么,我们明明限定的是只能是Apple的父类型啊,为什么传入父类型还会报错呢?
我们可以换个角度想一想,我们传入一个对象进来,可定要调用它的某个方法,但是我们如果传入的是父类,可能会没有子类的方法,但是子类一定拥有父类的方法,这样可能逻辑上就说的通了,这里是我自己的理解,如有不对大家请指出。

通过上面的代码我们可以发现,extends是为了安全的访问数据,而super是为了安全的写入数据。

7、虚拟机是如何实现泛型的

java实现泛型使用了类型擦除

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

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

这段代码在虚拟机中会变成下面这样

public class GenericType<Object> {
    private Object data;

    public Object getData() {
        return data;
    }

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

也就是泛型T变成了Object,这也就是所谓的泛型擦除
如果是下面这种状况

public class GenericType<T extends ArrayList & Comparable> {
    private T data;

    public T getData() {
        return data;
    }

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

如果是上面这种情况,T则会变成ArrayList。如果使用了限定符,则会转为限定符后面的第一个类型。
如果调用了Comparable相关的方法,则会在变量前面进行强转,即下面这种情况。

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

接下来我们看下面这段代码

    public static void ListMethod(List<String> list){
        
    }

    public static void ListMethod(List<Integer> list){

    }

我们知道方法的重载是参数列表不同,但是上面的代码编译器是会报错的,也是因为泛型擦除,即List参数类型都变为了List<Object>了。

最后我们看下泛型是怎么生效的

public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("test","aaaa");
        System.out.println(map.get("test"));
    }
//编译为class之后样式
public static void main(String[] args) {
        Map<String, String> map = new HashMap();
        map.put("test", "aaaa");
        System.out.println((String)map.get("test"));
    }

我们发现在字节码中对map.get("test")进行了强转,这也就是泛型的实现方式,强转。

泛型擦除并不是真正意义上的擦没了,泛型的类型会保存在字节码文件中,这样才实际调用的时候回进行强转。

以上就是泛型相关的一些知识了,如果有哪些不对的地方,希望大家指出,共同进步。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352