1. 泛型

1. 概念

一个类或接口,它的声明有一个或多个类型参数( type parameters ),被称之为泛型类或泛型接口。
在没有泛型的时候,对于一个类中有不知类型的变量通常要对其进行强转,变成Object类型,例如:

  1. public class Cache {
  2. Object value;
  3. public Object getValue() {
  4. return value;
  5. }
  6. public void setValue(Object value) {
  7. this.value = value;
  8. }
  9. }

上舒代码中,geValue和setValue的返回值,参数对应都是Object,现在Cache类可以缓存任何类型的实例,如下

  1. Cache cache = new Cache();
  2. cache.setValue(134);
  3. int value = (int) cache.getValue();
  4. cache.setValue("hello");
  5. String value1 = (String) cache.getValue();

只要对需要存入的类型进行强转就行,这是在Java5之前没有泛型时的做法。
现在引入泛型,所谓的泛型就是对需要传入的类型进行参数化。普通的函数参数如下,test方法的输入参数只能是int,那么我们可以对输入的这个类型参数化用E表示,相当于输入的类型也是个变量,但是这个类型变量定了以后不能改变,那么可以传入任何类型的类作为输入类型。

  1. public void test(int i){
  2. System.out.println(i);
  3. }
  4. //这就是泛型化后的
  5. public void set(E i){
  6. System.out.println(i)
  7. }

上面的Cache类使用泛型后:

  1. /**
  2. * @author: qujundong
  3. * @date: 2020/11/28 下午5:12
  4. * @description:
  5. */
  6. public class Cache<T> {
  7. private T value;
  8. public void setValue(T value){
  9. this.value = value;
  10. }
  11. public T getValue(){
  12. return value;
  13. }
  14. public static void main(String[] args) {
  15. Cache<Integer> cache = new Cache();
  16. cache.setValue(134);
  17. int value = (int) cache.getValue();
  18. System.out.println(value);
  19. }
  20. }

这个的好处就是不需要强转了,同时也更加安全,因为我们在声明这个Cache类的时候给定需要的类型,并且在后续的时候用的都是这个。

2. 泛型类

我们可以这样定义一个泛型类。

  1. public class Test<T> {
  2. T field1;
  3. }

尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

  1. public class Test<Hello> {
  2. Hello field1;
  3. }

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

  1. T 代表一般的任何类。
  2. E 代表 Element 的意思,或者 Exception 异常的意思。
  3. K 代表 Key 的意思。
  4. V 代表 Value 的意思,通常与 K 一起配合使用。
  5. S 代表 Subtype 的意思,文章后面部分会讲解示意。

如果一个类被 <T>的形式定义,那么它就被称为是泛型类。例如:

  1. Cache<Integer> cache1 = new Cache<>();
  2. Cache<String> cache2 = new Cache<>();

泛型可以接受多个泛型

  1. /**
  2. * @author: qujundong
  3. * @date: 2020/11/28 下午5:23
  4. * @description:
  5. */
  6. public class CacheMultiType<E, T> {
  7. private E value1;
  8. private T value2;
  9. public void setValue1(E value){
  10. this.value1 = value;
  11. }
  12. public void setVaule2(T value){
  13. this.value2 = value;
  14. }
  15. public static void main(String[] args) {
  16. CacheMultiType<String, Integer> cache = new CacheMultiType<>();
  17. cache.setValue1(new String("ssss"));
  18. cache.setVaule2(new Integer(3));
  19. }
  20. }

2. 泛型方法

下面是泛型方法和泛型类结合的使用,从中可以看出来,泛型类和泛型方法表示符号如果同名,并不会发生冲突,但是为了易读性,通常不设置为相同的。

 * @author: qujundong
 * @date: 2020/11/28 下午5:26
 * @description:
 */
public class CacheMethod<T> {
    public <T> T testOneType(T value) {
        System.out.println("one type");
        return null;
    }
    public <T, E> E testTwoTypes(T value){
        System.out.println("two types");
        return null;
    }

    public static void main(String[] args) {
        CacheMethod<Constant> cache = new CacheMethod();
        cache.testOneType(new String("ss"));
        cache.testOneType(123);
        cache.testTwoTypes(new Date());
    }

3. 泛型接口和泛型类相似

public interface<T> ICache{
        public T getVar() ;    // 定义抽象方法,抽象方法的返回值就是泛型类型
}

// 实现类直接指定泛型类型,后续使用不需要声明
class Cache implements ICache<String>{    // 定义泛型接口的子类
    private String var ;                // 定义属性
    public Cache(String var){        // 通过构造方法设置属性内容
        this.setVar(var) ;    
    }
    public void setVar(String var){
        this.var = var ;
    }
    public String getVar(){
        return this.var ;
    }
}


public class GenericsDemo25{
    public static void main(String arsg[]){
        Info i = null;        // 声明接口对象
        i = new Cache("...") ;    // 通过子类实例化对象
        System.out.println("内容:" + i.getVar()) ;
    }
}

// 生成对象的时候需要声明泛型
class Cache<T> implements ICache<T>{    // 定义泛型接口的子类
    private T var ;                // 定义属性
    public Cache(T var){        // 通过构造方法设置属性内容
        this.setVar(var) ;    
    }
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
};
public class GenericsDemo24{
    public static void main(String arsg[]){
        Info<String> i = null;        // 声明接口对象
        i = new Cache<String>("...") ;    // 通过子类实例化对象
        System.out.println("内容:" + i.getVar()) ;
    }
}

2. 通配符

1. 无限制通配符

通配符在使用的时候用<?>表示,在我理解,泛型可以认为是一个实参,确定了以后不能改变,对应的类只能是对应的泛型及其子类,而通配符可以了解为“形参”,因为我们在写代码的时候并没有确定对应的参数,所以无法添加,每个通配符可以表示任何类型,在读取数据的时候非常方便。

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

/**
 * @author: qujundong
 * @date: 2020/11/28 下午5:51
 * @description:
 */
public class CacheWildCard{
    public void readCache(List<?> value){
        for(Object v : value){
            System.out.println(v);
        }
    }
    public void writeCache(List<?> value){
        //无法添加,因为我们根本不知道?的类型,都没办法new对象
        value.add(null);
    }

    public static void main(String[] args) {
        Integer[] array = {1, 2, 3};
        ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(array));
        CacheWildCard cache = new CacheWildCard();
        cache.readCache(list);
        cache.writeCache(list);
        cache.writeCache(list);
        System.out.println(list);
    }
}
/*
1
2
3
[1, 2, 3, null, null]
 */

从上述代码中其实就可以看出为什么通配符无法添加数据,我们在读数据的时候都是用Object类型来便利的,那说明我们根本无法确定List<?>中?的类型,这个时候new的对象都不知道是什么,所以通配符无法添加数据。但是我们可以在list中添加null,因为添加null不需要确认类型。

2. 上界通配符

上界通配符表示为<? extends T>,表示可以接收T及其子类的对象。这里要声明一些,即使声明了上界,依然无法通过编译器添加元素
图片.png

/**
 * @author: qujundong
 * @date: 2020/11/28 下午6:08
 * @description:
 */
public class UpperCacheWildCard {
    public void readCache(List<? extends Person> value){
        for(Person v : value){
            System.out.println(v);
        }
    }
    public void writeCache(List<? extends Person> value){
        //无法添加
//        value.add(new Person(1, "name", LocalDate.now()));
    }

    public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person(1, "name", LocalDate.now()));
        UpperCacheWildCard cache = new UpperCacheWildCard();
        cache.readCache(list);
    }
}

为什么无法上界通配符无法添加数据其实很容易想清楚,如果代码中Person不是类,而是个接口,那么我们就无法生产Person实例

3. 下界通配符

下界通配符表示成<? supper T>,与无界通配符和上界通配符不同,有一定的添加数据能力,可以添加类型T的数据。

package item26;

import Entity.Man;
import Entity.Person;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: qujundong
 * @date: 2020/11/28 下午6:27
 * @description:
 */
public class CacheLowerCacheWilderCard {
    public void readCache(List<? super Man> value){
        for(Object v : value){
            System.out.println(v);
        }
    }
    public void writeCache(List<? super Man> value){

        value.add(new Man(1, "ss", LocalDate.now(), true));
        //编译无法通过
        //value.add(new Person(2, "dd", LocalDate.now()));
    }

    public static void main(String[] args) {
        ArrayList<Man> list = new ArrayList<>();
        list.add(new Man(234, "sssdd", LocalDate.now(), false));
        CacheLowerCacheWilderCard cache = new CacheLowerCacheWilderCard();
        cache.readCache(list);
        cache.writeCache(list);
        System.out.println(list);
    }
}
Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}
[Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}, Person{id=1, name='ss', localDate=2020-11-28} Man{sex=true}]

下界通配符之所以可以添加数据,是因为下界Man肯定是一个类,那么可以通过new Man()生成实例。

3. 有界泛型

可以参照通配符,对泛型加上界限 ,例如,从下面的例子可以看出,可以有public void readUpperValue(List values, T flag),这种声明,因为泛型擦除会将元素都转成类型T,不会有编译器报错,也不会因为类型不匹配而报错。例如最下面我将一个字符串强转成泛型的类型,不会报错,添加成功。但是如果乱进行类型转换就会出问题
对于泛型加上下界线会报错,不太懂,以后再搞

package item26;

import Entity.Man;
import Entity.Person;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: qujundong
 * @date: 2020/11/28 下午6:48
 * @description:
 */
public class CacheLowerAndUpperGeneric {
    public <T, E extends T> void readUpperValue(List<E> values, T flag){
        for(T t : values){
            System.out.println(t);
        }
    }
    public <T, E extends T> void writeUpperValue(List<E> values, T flag){
        values.add((E)new  Man(234, "sssdd", LocalDate.now(), false));
        values.add((E)new String("好奇怪"));
    }
    //这里编译无法通过,会报错
//    public <T, E super T> void readLowerValue(List<E> values, T flag){
//        for(T t : values){
//            System.out.println(t);
//        }
//    }
//    public <T, E super T> void writeLowerValue(List<E> values, T flag){
//        values.add((E)new  Man(234, "sssdd", LocalDate.now(), false));
//        values.add((E)new String("好奇怪"));
//    }




    public static void main(String[] args) {
        CacheLowerAndUpperGeneric cache = new CacheLowerAndUpperGeneric();
        ArrayList<Person> list1 = new ArrayList<>();
        Person p1 = new Person(123, "ds", LocalDate.now());
        Person p2 = new Man(234, "sssdd", LocalDate.now(), false);
        list1.add(p1);
        cache.readUpperValue(list1, p1);
        cache.writeUpperValue(list1, p1);
        System.out.println(list1);

    }
}
Person{id=123, name='ds', localDate=2020-11-28}
Person{id=234, name='sssdd', localDate=2020-11-28} Man{sex=false}
[Person{id=123, name='ds', localDate=2020-11-28}, Person{id=234, name='sssdd', 
localDate=2020-11-28} Man{sex=false}, Person{id=234, name='sssdd', 
localDate=2020-11-28} Man{sex=false}, 好奇怪]

4. 类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 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。
泛型信息被擦除了。

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 类型了。