我真的了解泛型吗?

前言

曾经年少轻狂的我,以为自己已经轻松拿捏Java泛型,直到后来学习Java 8新特性时,被泛型轻松反拿捏!那时的心情,正如标题一样,自我怀疑:"我真的了解泛型吗?" 答案很明显,不了解!要不是有eclipse/idea这类编辑器工具提供编译检查的话,真让我手写代码话,可能会写出让人笑掉大牙的代码。于是,再次回头来学习一下泛型知识。

泛型由来

很久很久以前。。。

记得小时候,奶奶提了一篮子鸡蛋去赶集卖鸡蛋,结果把几个老鸭蛋也卖了。原来是不经意间把几个鸭蛋混在鸡蛋一起了,幸好没有把鹅蛋也当做鸡蛋卖了,鹅蛋可比鸡蛋贵很多哦。于是,为了便于区分,爷爷就专门用竹子编织了几个不同的篮子专门用于盛放鸡蛋、鸭蛋、鹅蛋。用代码体现,如下:

  • 装鸡蛋的篮子
arduino 复制代码
/**
 * 蛋
 */
class Egg {}

/**
 * 鸡蛋
 */
class ChickenEgg extends Egg {}

/**
 * 装鸡蛋的篮子
 */
class BasketForChickenEgg {
    int capacity = 30;
    ChickenEgg[] container = new ChickenEgg[capacity];
    int size;

    public void  add(ChickenEgg chickenEgg) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = chickenEgg;
    }

    public ChickenEgg get() {
        if (size >= 0) {
            return container[size--];
        }
        return null;
    }
}
  • 装鸭蛋的篮子
arduino 复制代码
/**
 * 蛋
 */
class Egg {}

/**
 * 鸭蛋
 */
class DuckEgg extends Egg {}

/**
 * 装鸭蛋的篮子
 */
class BasketForDuckEgg {
    int capacity = 30;
    DuckEgg[] container = new DuckEgg[capacity];
    int size;

    public void  add(DuckEgg duckEgg) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = duckEgg;
    }

    public DuckEgg get() {
        if (size >= 0) {
            return container[size--];
        }
        return null;
    }
}
  • 装鹅蛋的篮子
arduino 复制代码
/**
 * 蛋
 */
class Egg {}

/**
 * 鹅蛋
 */
class GooseEgg extends Egg {}

/**
 * 装鸭蛋的篮子
 */
class BasketForGooseEgg {
    int capacity = 30;
    GooseEgg[] container = new GooseEgg[capacity];
    int size;

    public void  add(GooseEgg gooseEgg) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = gooseEgg;
    }

    public GooseEgg get() {
        if (size >= 0) {
            return container[size--];
        }
        return null;
    }
}

从上面的代码可以看出,几个装蛋的篮子类,除了蛋的类型不一样,其他的变量、方法实现逻辑都一样。那时候java还没有泛型,哪怕只有一个变量的类型不一样,也得写出不同类来区分,比如后面家里养了鸽子,得编织一个专门盛放鸽子蛋的篮子,再后来家里养了鸵鸟(这个有点夸张了,哈哈),又得编织一个专门盛放鸵鸟蛋的篮子。这样一直下去的话,就必须编织成百上千个不同类型的篮子了。

此时,你肯定会说,直接搞一个专门盛放蛋的篮子,不管什么蛋都可以装,如下:

arduino 复制代码
/**
 * 蛋
 */
class Egg {}

/**
 * 装蛋的篮子
 */
class BasketForEgg {
    int capacity = 30;

    Egg[] container = new Egg[capacity];

    int size;

    public void  add(Egg egg) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = egg;
    }

    public Egg get() {
        if (size >= 0) {
            return container[size--];
        }
        return null;
    }
}

如上,这个篮子有点特殊,可以装各种蛋,如:鸽子蛋、鹌鹑蛋、鸡蛋。。。但是,小时候的我,非常调皮,经常偷偷的把鸽子蛋和鹌鹑蛋捡到一起,由于这两种蛋大小、颜色都非常相似,奶奶拿到街上去卖的时候,就不好分辨了,很容易卖错价钱!

对比咱们曾经写过的业务代码,有没有发现和装蛋的篮子故事很相似:有某些类的实现算法、逻辑几乎相同,只是个别参数或变量的类型不同,但是根据业务需要,我们不得不写很多这些极其相似的类或者方法,如果有成千上万这样的不同类型的变量,就得写成千上万个非常类似的类,这就导致了非常严重的代码冗余问题了。

JDK1.5以后。。。

于是,为了解决这一问题,我们把这样的在通用模板中,存在变化的的东西提取出来,用参数化表示 ,即用T来表示。如,BasketForEgg就是一个模板类,只需要指定这个T为ChickenEgg,就可以得到Basket类,指定T为DuckEgg便可得到Basket类,这样就不用写成千上万个类啦,只需要一个模板类就搞定了,换句话说就是通过通用模板类,达到制作完全不同类的目的。如下图所示:

通过BasketForEgg这个模板类,制作出了BasketForEgg、BasketForEgg、BasketForEgg三个不同的类。虽然,目前java在运行期间会存在擦除,这只是java实现泛型的一种机制,但是在逻辑上(编译层面) 我们把它们当做三个完全不同的类,你能用BasketForEgg basket = new BasketForEgg()吗?显然不能,编译不通过。所以在有了泛型之后,要把类名和泛型参数绑在一起来看,才能算一个独立的类,如BasketForEgg是一个类,而不是BasketForEgg类。

一句话总结:泛型类是类的模版,类是对象实例的模版! 有了泛型之后,再也不用写那么多重复的类了!

【effective java】的作者:泛型类有点像普通类的工厂,其实就是一个意思啦

泛型定义

泛型即参数化类型,通常用T来占位,如需多个就用多个大写字母来占位,如E、K、V等,通过填充不同类型的参数,便能得到填充类型的类,常用在类、接口、方法上。如BasketForEgg就是一个泛型类,而Egg、DuckEgg这些都是具体的类。jdk1.5后,几乎所有的集合类都支持了泛型,常见的开源框架也纷纷支持泛型,jdk8中Stream流更是把泛型的应用体现得淋漓尽致。

泛型分类

根据泛型参数T,所在的位置不同,可以分为泛型接口、泛型类、泛型方法, 其实接口和类是一个意思,也可以分为两种泛型方法、泛型类/接口。

泛型类

泛型参数T写在类名的后面,通过尖括号表示,写在类上面的泛型参数T,它的作用域自然就是整个类了。那一般什么时候是去填充这个T的具体类型呢?常见的两种方式:

  1. 在对这个泛型类实例化时填充T的具体类型
  2. 在子类继承这个泛型类时填充T的具体类型
  3. 如果子类在继承该泛型类时,不填充具体的类型,那么子类也可以是泛型类
scala 复制代码
/**
 * 装蛋的篮子
 */
class BasketForEgg<T> { 
    int capacity = 30;

    Object[] container = new Object[capacity];

    int size;

    public void  add(T t) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = t;
    }

    public T get() {
        if (size >= 0) {
            return (T) container[size--];
        }
        return null;
    }

    public static void main(String[] args) {
        //实例化对象时,填充具体的泛型类型,这里以鸭蛋为例
        BasketForEgg<DuckEgg> duckEggBasket = new BasketForEgg<DuckEgg>();
        
        //装一个鸭蛋,正常
        duckEggBasket.add(new DuckEgg());
        
        //编译器报错,因为指定了泛型类型为DuckEgg,就只能装鸭蛋了
        duckEggBasket.add(new ChickenEgg()); 
    }

    /**
     * 子类在继承泛型类时,指定具体的泛型类型
     */
    class SubBasketForEgg extends BasketForEgg<DuckEgg> {}

    /**
     * 子类在继承泛型类时,若未填充具体的类型,则子类也是一个泛型类
     */
    class SubBasketForEgg2<T> extends BasketForEgg<T> {}
}

泛型接口

泛型参数T写在类接口名的后面,通过尖括号表示。同泛型类基本一致,泛型参数T作用域在整个接口中,在某个具体的类实现泛型接口时,填充具体的泛型类型;在子类接口继承泛型接口时,可以填充具体的参数类型,或者不填充,该子类接口也可以是泛型接口。如下:

  • 装东西的篮子
csharp 复制代码
/**
 * 装东西的篮子接口:泛型接口
 */
interface Basket<T> {
    void add(T t);
    T get();
}
  • 装水果的篮子
typescript 复制代码
/**
 * 装水果的篮子:在实现篮子接口时,填充具体的泛型类型
 * 这里要把Basket<Fruit>看成一个整体,BasketForEgg实现的是Basket<Fruit>接口,
 * 而不是Basket接口
 */
class BasketForFruit implements Basket<Fruit> {
    int capacity = 30;

    Object[] container = new Object[capacity];

    int size;

    @Override
    public void add(Fruit fruit) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = fruit;
    }

    @Override
    public Fruit get() {
        if (size >= 0) {
            return (Fruit) container[size--];
        }
        return null;
    }


    public static void main(String[] args) {
        Basket basket = new BasketForFruit();
        basket.add(new Fruit());
    }
}

class Fruit {

}
  • 装蛋的篮子
arduino 复制代码
/**
 * 装蛋的篮子:在实现篮子接口时,指定填充的泛型类型
 * 注意这里要把Basket<Egg>看成一个整体,BasketForEgg实现的是Basket<Egg>接口,
 * 而不是Basket接口
 */
class BasketForEgg implements Basket<Egg> {
    int capacity = 30;

    Object[] container = new Object[capacity];

    int size;

    public void  add(Egg egg) {
        if (size >= capacity) {
            throw new RuntimeException("basket is full!");
        }
        container[size++] = egg;
    }

    public Egg get() {
        if (size >= 0) {
            return (Egg) container[size--];
        }
        return null;
    }

    public static void main(String[] args) {
        BasketForEgg duckEggBasket = new BasketForEgg();
        duckEggBasket.add(new DuckEgg());
    }
}

泛型方法

泛型方法,是指泛型参数作用与方法上,一般是在调用方法时,通过入参或者接收返回值的引用指定具体的类型。这里以方法所在的类是非泛型类,只有方法为泛型方法的前提下为例。

csharp 复制代码
public <T> void print() {
    List<T> list = new ArrayList<>();
    System.out.println(list.size());
}

非泛型类中的泛型方法,一般 放在返回值类型之前,public权限修饰符、static静态修饰符之后,表示该方法是一个泛型方法

泛型方法的T作用的位置有可以有三处:方法体(方法内部逻辑中)、返回值、入参。可以只作用于一处,也可以两两接结合,甚至三处同时都有,具体场景分析,如下。

  • 占位符T只在入参位置不参与方法体逻辑,也不影响返回值
csharp 复制代码
public class GenericMethodDemo {
    /**
     * 只有入参传入了T, 方法体和返回值都未用到T,所以不管传入List<String> 
     * 还是List<Integer>,不影响此方法   
     */
    public static <T> void method1(List<T> list) {
        System.out.println(list.size());
    }
  	/**
  	 * 把上面的泛型方法改成此种非泛型方法,因为没有必要
  	 */
    public static void method11(List<?> list) {
        System.out.println(list.size());
    }
  
}
  • T只出现在方法体中,入参和返回值都没有T
csharp 复制代码
/**
 * 占位符T在方法体中有用到,入参和返回值都没有用到
 */
public static <T> int method2() {
    List<T> list = new ArrayList<T>();
    return list.size();
}

/**
 * 我们用占位符T,目的就是想在调用方法时填充这个T,method2中这个T毫无意义
 * 所以可以直接改成非泛型方法,像method3这样
 */
public static int method3() {
    List<?> list = new ArrayList<>();
    return list.size();
}
  • 返回值中有T,入参和方法体中没有T
csharp 复制代码
//编译不同过,方法体都没有T,怎么会返回T呢,所以不存在这种说法 
public static <T> T method4() {
    return 1; 
}
  • 入参没有T,方法体和返回值都有T
csharp 复制代码
/**
 * 这里只有方法体中和返回值中有T,那么这个T什么时候填充的?
 */
public static <T> List<T> method5() {
    List<T> list = new ArrayList<>();
    return list;
}

public static void main(String[] args) {
    //这里在引用接收的时候填充了T,这使得同一个方法返回多种不同类型的值
    List<String> list = method5();
    list.add("aa");
    List<Integer> list1 = method5();
    list1.add(1);

}

这种方式有什么意义呢,通过一个方法可以快速得到指定泛型的List

  • 方法入参有T,返回值有T,方法体没有T
ini 复制代码
public static void main(String[] args) {
    //通过传入的参数来确定T的类型
    List<String> list = Arrays.asList("aa", "bb");
    String s = method6(list);

    List<Integer> list1 = Arrays.asList(11, 22);
    Integer integer = method6(list1);
    
}

/**
 * 方法入参有T, 返回值有T,方法体中没有T
 */
public static <T> T method6(List<T> list) {
    return list.get(0);
}
  • 入参有T,方法体有T,但是返回值没有T
ini 复制代码
/**
 * 方法入参有T, 返回没有T,方法体有T
 */
public static <T> int method7(List<T> list) {
    T t1 = list.get(0);
    T t2 = list.get(1);

    Map<String, T> map = new HashMap<>();
    map.put("aaa", t1);
    map.put("bbb", t2);
    return map.size();
}

/**
 * method8和method7区别,好像都一样,但是语义不一样
 */
public static int method8(List<?> list) {
    Object t1 = list.get(0);
    Object t2 = list.get(1);

    Map<String, Object> map = new HashMap<>();
    map.put("aaa", t1);
    map.put("bbb", t2);
    return map.size();
}

/**
 * method9
 */
public static <T, K> boolean method9(List<T> list, List<K> list1) {
    T t1 = list.get(0);
    T t2 = list.get(1);

    Map<String, Object> map = new HashMap<>();
    map.put("aaa", t1);
    map.put("bbb", t2);

    K k1 = list1.get(0);
    K k2 = list1.get(1);
    K k3 = list1.get(2);

    Map<String, Object> map1 = new HashMap<>();
    map1.put("ccc", k1);
    map1.put("ddd", k1);
    map1.put("eee", k3);

    return map.size() > map1.size();
}

如上代码所示,对比method7和method8发现,方法入参有T,方法体内用到了T,返回值没有T,这里就不会在乎T传入的是什么类型了,都不会影响方法体的执行,如果把List改成List<?>也不会有任何影响,唯一要变化的就是如method8中,把T换成Object,方法体依然正常执行,但是这样从某种意义上来说破坏了语义,例如我明明传给你的是装鸭蛋的篮子(List), 你却把装鸭蛋的篮子变成了装蛋的篮子(List), 这不是又混淆了吗?所以这里语义上要求不能变,就需要时要具体的泛型T,而不是通配符" ?"。

再例如method9方法中,有两个入参,其实都改成List<?>也不是不可以,只是语义不清晰,所以还是使用泛型方法比较好,虽说方法体执行结果都一样,但是代码语义清晰,更方便阅读、理解

总结:方法入参有T,方法体用到了T,但是返回值无T,无论List填充什么类型都不影响具体的执行结果,只是在语义上有差别

针对此种类型,举个JDK中的栗子:

csharp 复制代码
interface Stream<T> {
    //这里T因为接口上已经有了,先忽略,只看泛型方法中的R、A
    <R, A> R collect(Collector<? super T, A, R> collector);
}

public final class Collectors {
  
   public static Collector<CharSequence, ?, String> joining() {
          return new CollectorImpl<CharSequence, StringBuilder, String>(
                  StringBuilder::new, StringBuilder::append,
                  (r1, r2) -> { r1.append(r2); return r1; },
                  StringBuilder::toString, CH_NOID);
   }

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
          return joining(delimiter, "", "");
    }
  
   public static <T> Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }
  
}

//现在jdk8下用得最常用的Stream流API,它的collect方法是一个泛型方法,占位符有R、A,入参也有R和A,但是返回值只有R,没有A,根据上面所讲的,A这个泛型参数不在乎它具体是什么类型,即使方法体有用到A,返回值只有R,那么在转入Collector时,这个A就可以用?来代替。

//再看Collecters类中的joining()、toList()方法,返回的恰好就是Collector<CharSequence, ?, String>,用?指定了A。
  • 方法人参、方法体、返回值都有T,这是最常见的一种方式
ini 复制代码
public static <T> T method10(Class<T> clazz) {
    T t1 = null;
    try {
        t1 = clazz.newInstance();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return t1;
}

泛型引用

在java语言中,数据类型可以分为两大类:基础数据类型引用数据类型 ,那么加上泛型参数后的引用类型,这里姑且叫做泛型引用类型,泛型引用和其他对象引用不同,它通常只能指向它自己类型的实例。

普通的泛型引用

还是以装蛋的篮子为例,这里为了简化代码,使用jdk的List类代替Basket,ArrayList这个泛型引用,只能指向装鸭蛋的对象实例,如下:

scala 复制代码
/**
 * 装蛋的篮子
 */
public class BasketDemo {
    
    public static void main(String[] args) {
    	//ArrayList<DuckEgg> 与 ArrayList<ChickenEgg>是两个完全不同的类型
        //这里编译报错,ArrayList<DuckEgg>只能指向new ArrayList<DuckEgg>()实例
        ArrayList<DuckEgg> duckEggArrayList1 = new ArrayList<ChickenEgg>();

        //正常通过
        ArrayList<DuckEgg> duckEggArrayList = new ArrayList<DuckEgg>();
    }
}

/**
 * 蛋
 */
class Egg {}

/**
 * 鸡蛋
 */
class ChickenEgg extends Egg {}

/**
 * 鸭蛋
 */
class DuckEgg extends Egg {}

/**
 * 鹅蛋
 */
class GooseEgg extends Egg {}

那么问题来了,DuckEgg是Egg的子类,那ArrayList可以指向new ArrayList()对象实例吗?

答案很明显,编译报错!这里再次强调文章开头的观点,ArrayList与ArrayList在编译器的眼里是两个完全不同的具体类,即使Egg是DuckEgg的父类,所以这里类型不匹配,编译不通过,不能这么指定。

问题又来了,由于ArrayList是实现了List接口的,那List泛型类型可以指向ArrayList吗?

typescript 复制代码
public class BasketDemo {
    public static void main(String[] args) {
        List<Egg> duckEggArrayList = new ArrayList<Egg>();
    }
}

答案是肯定的。因为ArrayList本身就实现了List接口,所以在实例化ArrayList时,填充E为Egg类型,自然而然ArrayList就实现了List接口,使用父类引用指向子类实现,这不就是咱们的多态嘛!这里把ArrayList看着是一个整体类,List看做一个整体类,ArrayList是实现了List接口的,所以这里是可以的,也是经常用得最多的方式。

那如何让一个List这样的引用,既可以指向ArrayList、又可以指向ArrayList呢?

通配符"?"泛型引用

为了让一个泛型引用,指向它的多个不同类型的实例,这里需要使用到通配符"?",通过List<? extends Egg>表示只要是Egg及其子类,它都能认识,如下:

typescript 复制代码
public class BasketDemo {
    public static void main(String[] args) {
        //反应引用List<? extends Egg>指向了ArrayList<DuckEgg>实例
        List<? extends Egg> eggList = new ArrayList<DuckEgg>();
        //引用变量eggList又指向了ArrayList<ChickenEgg>实例
        eggList = new ArrayList<ChickenEgg>();
        //引用变量eggList又指向了ArrayList<GooseEgg>实例
        eggList = new ArrayList<GooseEgg>();
    }
}

如上代码所示,List<? extends Egg>在这里是一个引用标识 ,并没有任何具体的List<? extends Egg>类型,所以也不能直接使用new关键字new一个ArrayList<? extends Egg>()实例,它的作用就是扩大泛型引用的范围,它可以指向多个填充不同类型的具体类,如:ArrayList、ArrayList()、ArrayList、ArrayList。

那么问题来了,像List<? extends Egg>这样的引用指向的实例对象,可以正常CRUD吗?

如上代码所示,在往List<? extends Egg>里面添加一个new Egg()时,编译器提示错误,这是因为List<? extends Egg>是一个泛型引用,在使用eggList这个引用变量添加元素时,还是会受到泛型引用接口的限定,它可能指向一个装鸭蛋的list实例,也可能指向一个装鸡蛋的list实例,它到底指向哪个具体的类型,它不知道,所以这里它不敢乱添加,但是它知道我这里面装的就是Egg类型,get出来的一定是Egg类型。

这就是常见的上下边界问题,通过这种方式是为了扩大泛型引用的范围。当然还有个List<? super Egg> 泛型引用,和extends一样,只不过方向相反。

问题又双叒来了,像这样的List<? extends Egg>的泛型引用,不能添加元素,不能修改原素,又有什么意义呢?

typescript 复制代码
public class GenericTest {
    public static void main(String[] args) {
        List<DuckEgg> duckEggs = new ArrayList<DuckEgg>();
        List<ChickenEgg> chickenEggs = new ArrayList<ChickenEgg>();

        iterateList(duckEggs);
        iterateList(chickenEggs);
    }

    /**
     * 主要目的用于非修改遍历
     */
    public static void iterateList(List<? extends Egg> list) {
        for (Egg egg : list) {
            System.out.println(egg);
        }
    }
}

如上代码所示,由于List<? extends Egg> list可以指向多个具体的实例,又不能对其进行修改,所以这里的主要目的就是非修改遍历

再看看List<?>这个泛型引用,这个 ?没有上下边界了,它不知道指向具体哪个类型,所以它泛化的对象就是Object,在Java里面Object就是根对象,是一切对象的父对象,所以通过调用get方法,得到的必然也是Object类型的。

typescript 复制代码
public class GenericTest {
    public static void main(String[] args) {
        List<DuckEgg> duckEggs = new ArrayList<DuckEgg>();
        List<ChickenEgg> chickenEggs = new ArrayList<ChickenEgg>();

        iterateList(duckEggs);
        iterateList(chickenEggs);
    }

    /**
     * List<?> 更进一步,可以指向填充任意类型的实例对象
     */
    public static void iterateList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

获取泛型类型

有的时候需要获取到具体的泛型类型来做一些业务判断,或者在研发一些框架时,需要获取到该类填充的具体类型,所以有必要了解下如何获取泛型类型。

  1. 如何获取本类的泛型具体类型?
csharp 复制代码
public class GenericTest {
    public static void main(String[] args) {
        GenericTest genericTest = new GenericTest();
        genericTest.getGenericType();
    }
    public void getGenericType() {
        //这里在编译期指定为鸭蛋,但是运行期,会被擦除,如:BasketForEgg basketForEgg = new BasketForEgg();
        //所以这样是拿不到具体的泛型类型的。
        //BasketForEgg<DuckEgg> basketForEgg = new BasketForEgg<>();

        BasketForEgg<DuckEgg> basketForEgg = new BasketForEgg<>(DuckEgg.class);
        System.out.println(basketForEgg.getClazz().getSimpleName()); //DuckEgg

    }

    /**
     * 装蛋的篮子
     */
    class BasketForEgg<T> {
        //用于获取具体的泛型类型,无其他作用
        Class<T> clazz;

        public BasketForEgg(Class<T> clazz) {
            this.clazz = clazz;
        }

        public Class<T> getClazz() {
            return clazz;
        }
    }

    class Egg{}

    class DuckEgg extends Egg{}
}
  1. 如何获取父类或者父接口的泛型类型?
scala 复制代码
public class GenericTest {
    public static void main(String[] args) {
        GenericTest genericTest = new GenericTest();
        genericTest.getGenericType();
        genericTest.getGenericType1();
    }

    /**
     * 获取父类泛型类型
     */
    public void getGenericType() {
        BasketForDuckEgg basketForDuckEgg = new BasketForDuckEgg();
        //通过class对象获取父类泛型参数类型,ParameterizedType是Type子类,所以这里需要强转一下
        ParameterizedType superclass = (ParameterizedType)basketForDuckEgg.getClass().getGenericSuperclass();
        //因为泛型参数可能有多个,所以这里拿到实际的泛型类型为一个数组
        Type[] actualTypeArguments = superclass.getActualTypeArguments();
        //得到结果:[class com.kang.mybatis.study.sort.GenericTest$DuckEgg]
        System.out.println(Arrays.toString(actualTypeArguments));
    }

    /**
     * 获取父接口的泛型类型
     */
    public void getGenericType1() {
        BasketForChickenEgg basketForChickenEgg = new BasketForChickenEgg();
        //通过class对象获取父接口泛型参数类型,存在实现多个接口的情况
        Type[] genericInterfaces = basketForChickenEgg.getClass().getGenericInterfaces();
        for (Type genericInterface : genericInterfaces) {
            ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            //执行结果就是鸡蛋啦:[class com.kang.mybatis.study.sort.GenericTest$ChickenEgg]
            System.out.println(Arrays.toString(actualTypeArguments));
        }
    }
    
    /**
     * 装鸭蛋的篮子
     */
    class BasketForDuckEgg extends BasketForEgg<DuckEgg> {

    }

    /**
     * 装鸡蛋的篮子
     */
    class BasketForChickenEgg implements Basket<ChickenEgg> {

    }

    interface Basket<T> {}

    /**
     * 装蛋的篮子
     */
    class BasketForEgg<T> {

    }

    class Egg{}

    class DuckEgg extends Egg{}

    class ChickenEgg extends Egg{}
}
  1. 泛型父类如何获取到自己的具体泛型类型呢?
scala 复制代码
public class GenericTest {
    public static void main(String[] args) {
        BasketForDuckEgg basketForDuckEgg = new BasketForDuckEgg();
        basketForDuckEgg.getGenericType();
    }

    /**
     * 装鸭蛋的篮子
     */
    static class BasketForDuckEgg extends BasketForEgg<DuckEgg> {
        /**
         * 通过子类获取填充的父类泛型类型给父类使用
         */
        public void getGenericType() {
            //BasketForEgg<DuckEgg>想要获取自己的泛型类型,通过其子类来实现
            ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass();
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            Class clazz = (Class) actualTypeArguments[0];
            System.out.println(clazz.getSimpleName());
            super.setClazz(clazz);
        }
    }

    /**
     * 装蛋的篮子
     */
   static class BasketForEgg<T> {
        //通过一个class变量来获取子类实现时指定的泛型类型
        Class<T> clazz;

        /**
         * 子类调用
         * @param clazz
         */
        public void setClazz(Class<T> clazz) {
            this.clazz = clazz;
            System.out.println(clazz.getName());
        }
    }

    class Egg{}

    class DuckEgg extends Egg{}

    class ChickenEgg extends Egg{}
}
  1. 总结
    1. 对于直接获取本类的泛型具体类型,通过一个成员变量来保存泛型的具体类型,因为泛型参数是通过new关键字来确定的,在运行期泛型已经被擦除了,所以是拿不到自己本类的泛型具体类型的;
    2. 而某个类继承某个泛型类或者实现某个泛型接口时,是可以直接拿到泛型父类/父接口,这是因为在编译期通过 extends、implements时确定尖括号这个T的类型的,编译后会把这个T类型保留在字节码里;
    3. 为什么会有获取具体泛型类型这样的需求?在一些开源框架中,随处可见这样的用法,例如spring的泛型注入,mybatis、hibernate等根据泛型类型做orm映射,还有咱们在导出Excel时,List也需要用到具体的泛型类型,来确定导出时列名称等。

泛型原理

这个就不用多说了,泛型目前针对java来说,就是一种语法糖,用来骗骗编译器的,把类型转化风险提前放在编译期解决。为了兼容老版本的jdk,实际的泛型会被擦除,所以编译后的class和之前的版本没什么区别。

展望未来,说不定以后的jdk更新版本中,会保留真实的泛型类型类,同C++一样。如ArrayList与ArrayList就是两个不同的类了呢。

在C++语言是保留了真实的泛型类的,只是目前java还没有

相关推荐
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 分钟前
idea 弹窗 delete remote branch origin/develop-deploy
java·elasticsearch·intellij-idea
Code成立4 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
鸽鸽程序猿28 分钟前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
遇见你真好。44 分钟前
自定义注解进行数据脱敏
java·springboot
NMBG221 小时前
[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
java·开发语言·面试·java-ee·intellij-idea
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
像污秽一样1 小时前
Spring MVC初探
java·spring·mvc
计算机-秋大田1 小时前
基于微信小程序的乡村研学游平台设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
LuckyLay1 小时前
Spring学习笔记_36——@RequestMapping
java·spring boot·笔记·spring·mapping
醉颜凉2 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法