Java泛型,这一篇就够了

1. 为什么我们需要泛型

现实世界中我们经常遇到这样一种情况,同一个算法/数据结构适用于多种数据类型,我们不想为每一种类型单独写一个实现。举个例子来说,我们有一个Pair类型,存储key、value两个字段,代码如下。如果有一天,我想存储Integer类型的key, Date类型的Value,这个时候,我需要重新定义一个Pair类型。

public class Pair {
    private String key;
    private String value;
}
public class PairIntDate {
    private Integer key;
    private Date value;
}

存储的需求是多种多样的,慢慢地我们会有一大堆PairXxxYyy类型,重复定义的Class会大量冗余。于是人们想到了第二个方案,用Object引用存储,使用的时候再做强制类型转换。

public static class Pair {
    public Object key;
    public Object value;
}

使用的时候,写入可以直接赋值,读取的时候强制类型转换。Java的ArrayList就是这么做的,通过维护一个Object[] elementData实现数据的存储。

Pair p = new Pair();
p.key = "stringKey";
p.key = Integer.valueOf(1);
String key = (String) p.key;

这种方法带来了两个问题:

  1. 读取时要强制类型转换
  2. 没有编译检查,能写入任意类型数据,导致读取时失败

Java 5开始引入的泛型就是为了解决这个问题的,使用泛型时,我们可以这样定义Pair并使用,完美的解决了上面两个问题。

public class RawArray {



    public static class Pair<K, V> {
        private K key;
        private V value;

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>();
        pair.setKey("pairKey");
        pair.setValue(Integer.valueOf(1));
        Integer localValue = pair.getValue();
    }      

}

2. 泛型的实现原理

2.1 类型擦除

JVM内并没有泛型这个概念,所有的类型都是普通的类。以上面的RawArray为例,我们看看Java内部是怎么做的。通过javap命令查看class文件的字节码,命令如下:

javap -verbose RawArray$Pair.class
javap -verbose RawArray.class

Pair类的字节码如图,实际存在的字段key、value都是Object类型,getKey方法实际返回值类型是Object,setKey的入参实际类型也是Object类型,这就是我们经常说的类型擦除,泛型会擦除到定义的上界。

下面我们看看main方法里是如何使用Pair对象的,调用setKey方法时的类型检测是编译器行为,在字节码中并没有体现。getValue方法返回的是Object类型对象,编译器插入了checkcast命令替我们完成类类型转换。

2.2 桥接方法

从上一节学习的Pair字节码我们知道,Pair类的setKey方法的参数、getKey方法的返回值都被擦除为Object类型。如果我们继承Pair, 并重写setKey、getKey方法。

public static class ExtendPair extends Pair<String, Integer> {

    public String getKey() {
        return super.getKey();
    }

    public void setKey(String key) {
        super.setKey(key);
    }
}

回忆一下Java的基础知识, Java的方法签名指的是方法名和参数列表,父类Pair有两个方法:

  1. Object getKey()
  2. void setKey(Object)

子类ExtendPair重写/新增了两个方法

  1. String getKey()
  2. void setKey(String key)

两个setKey的方法签名不同,实际是两个重载方法,而getKey方法签名相同,返回值却不同,Java语言规范内是不允许的。如果我将ExtendPair向上转型为Pair并调用getKey和getValue方法会怎么样呢?通过阅读ExtendPair的字节码,我们找到了答案。

Java语言规范中不允许同时定义String getKey()、Object getKey()两个签名相同的方法,但是字节码层面是允许的,通过ExtendPair的字节码,生成的Object getKey()会调用String getKey()方法,这个方法被称为桥接方法,向上转型为Pair后,实际调用的是Object getKey()方法,签名和父类完全一致,然后由它转发个String getKey()方法。

setKey的桥接方法实现和getKey如出一辙,这里就不详细讲解了,有兴趣可以看看对应的字节码。值得一提的是,在类继承时提到的协变返回类型也是通过桥接方法实现的。

3. Java泛型的定义

3.1 泛型方法

之前的案例里我们已经看到泛型类( RawArray$Pair)的定义了,泛型类的类型参数不能用到静态方法上。我们来看一下泛型方法的定义,类型参数放到修饰词之后,返回值之前,看示例的of方法。通常调用泛型方法时我们不需要明确地知道类型参数,编译器自己能通过入参/返回值接收对象的引用推断出类型参数。

public static class Pair<K, V> {
    private K key;
    private V value;

    public static <T,S> Pair<T,S> of(T k, S v) {
        Pair<T,S> r = new Pair<>();
        r.key = k;
        r.value = v;
        return r;
    }
}

通过这么使用我们定义的泛型方法

var aPair = Pair.of("pairKey",1);
System.out.println(aPair.getKey());

3.2 类型限定

讲Java泛型的原理的时候,我们讲过Java会将泛型的类型擦除,最后存储的是Object类型的引用。Java并不支持动态语言里的Duking Type,这个时候如果我们想调用Pair对象里key、value对象上的方法,我们只能调用Object上定义的方法。如果我们想调用指定类型下的方法,就需要用到类型限定。

我们通过一个例子来看,假设我们有一个Range类,接收两个值来表示一段区间,这两个值必须支持比较(Comparable),同时我们希望在较小的实例上执行某些操作(包装在Runnbale中),代码如下

public static class Range<T extends Comparable<T> & Runnable> {
    private T min;
    private T max;
    

    public static <S extends Comparable<S> & Runnable> Range<S> of(S s1, S s2) {
        if (s1.compareTo(s2) > 0) {
            S temp = s1;
            s1 = s2;
            s2 = temp;
        }
        s1.run();
        Range<S> range = new Range<>();
        range.min = s1;
        range.max = s2;
        return range;
    }
}

示例中的S extends Comparable<S> & Runnable就是我们说的类型限定,如果有多个限定类型用&号分隔,Java允许类继承一个父类多个接口,指定类型限定的时候,通过extends关键字,后面接一个0或1个父类,0或N个接口,如果有的话,父类限定要放到最前面。

可以看到Comparable.compareTo方法、Runnable.run方法都可以直接在泛型方法内部调用,虽然还是要求用接口做类型限定,比起Duking Type还略有缺憾,但应该说已经不错了。

3.3 通配符类型

假设我们有3个类,Animal表示动物,Dog是Animal的子类,Cage是一个泛型类,可以持有一个对象的引用,代码如下

public static class Animal {
}

public static class Dog extends Animal {
}

public static class Cage<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

各个类直接的关系如下图,Animal和Dog有继承关系,Cage和Cage并没有,最右侧的一列我们稍后再讲。

如果我们一个方法delivery用于投递Cage,如果我们使用Cage调用这个方法,编译器提示Required Cage, Provide Cage

private static void delivery(Cage<Animal> cage)

使用通配符可以解决这个问题,Cage表示持有某个Animal的子类,这个类型可以是Animal,也可以是Dog

private static void delivery(Cage<? extends Animal> cage)

除了子类型限定以外,还有超类型限定,无限定通配符

类型

语法

简记

描述

子类限定

Cage

能调Cage.getT方法,用Animal接受返回值;不能调Cage.setT,T是继承Animal的类,不知道确切类型

超类限定

Cage

能调Cage.setT方法,用Animal子类做入参;不能调Cage.getT,T是Animal的超类,不知道确切类型

无限定通配符

Cage

读Object

能调Cage.getT方法,用Object接收返回值;不能调Cage.setT,不知道T的确切类型

4. Java泛型的局限

4.1 类型检查只能用于原始类型

由于类型擦除的原因Pair和Pair都会将类型参数擦除到Object,所以这两个泛型类型的实例变量的Class实际是相同的

Pair<Integer,Integer> pi = new Pair<>();
Pair<Long,Long> pl = new Pair<>();
System.out.println(pi.getClass() == pl.getClass());

调用instanceof时,下面两个判断都会发true,而对pi instanceof Pair检查时,会报编译错误。

if (pi instanceof Pair<Integer, Integer>) {
    System.out.println("Pair<Integer,Integer>");
}
if (pi instanceof Pair) {
    System.out.println("Pair");
}

4.2 不能参数化类型数组

不能创建泛型类型的数组,数组会存储元素的类型,通过Class.getComponentType()获取数组元素的类型,Class.getComponentType()只能元素的原始类型,运行时无法做到阻止往Pair[]的数组中添加Pair的元素。但是编译器允许定义泛型数组的变量,定义一个Pair的数组后强制类型转换为泛型数组。

Java不允许我们创建泛型对象的数组,可变参数数组的时候这个限制略有放松,允许我们定义泛型的可变参数列表,下面这段代码是允许的

private void varArgs(Pair<String, Integer>... ps) {
    // code goes here
}

4.3 无法实例化类型变量

因为类型擦除的作用,我们无法通过T.class引用到Class对象,当然更无法通过T.class创建对象的实例。早期我们通过传入Class clazz来完成实例的创建。Java 8之后可以通过函数式接口来创建。

public static class Holder<T> {
    public T makeT() {
        return T.class.newInstance(); // 无法正常允许,不能通过T.class因为到Class对象
    }
    public T makeT(Class<T> clazz) throws Exception {
        return clazz.getConstructor().newInstance();
    }
    public T makeT(Supplier<T> supplier) {
        return supplier.get();
    }
}

泛型数组的创建也可以采用类似的方法,传入Class clazz或者传入一个函数式接口来创建

public static class Holder<T> {
    public T[] makeT(Class<T> clazz) {
        return (T[]) Array.newInstance(clazz, 10);
    }
    public T[] makeT(IntFunction<T[]> make) {
        return make.apply(10);
    }
}
// 调用方式
Holder<String> ss = new Holder<>();
System.out.println(Arrays.toString(ss.makeT(String.class)));
System.out.println(Arrays.toString(ss.makeT(String[]::new)));

4.4 不能定义/抛出/捕获泛型异常

继承自Throwable的类不能是泛型类型,catch的括号里不能用类型变量。下面的throwsAs是CoreJava里的一个示例,通过欺骗编译器允许方法不声明检查型异常。

public static <T extends Throwable> void throwsAs(Throwable t) throws T {
    throw (T) t;
}


public T makeT(Class<T> clazz) {
    try {
        return clazz.getConstructor().newInstance();
    } catch (Throwable t) {
        GenInstance.<RuntimeException>throwsAs(t);
        return null;
    }
}

5. Java泛型的反射

Java反射的信息最终来自.class文件,而在编译的时候我们并不知道类会怎么被使用。假设我们定义了Company>类,使用时Company、Company引用的是同一个Class对象。为了方便讲解,我们定义如下的测试类

public static class KeyNiuTech extends Company<Date> {
}

public static class Company<T extends Comparable<? super T>> {
    private T[] staff;
    private T aStaff;
    public static <O extends Comparable<? super O>> Company<O> of(O o) {
        Company<O> c = new Company<>();
        c.aStaff = o;
        return c;
    }
}

通过查看这两个的字节码可以看到,这两个能拿到的信息的上限,Company能拿到类型参数T以及T的上限Comparable,以及Comparable的类型参数。KeyNiuTech里更有意思一点,KeyNiuTech继承自Company,这里的这个类型Date也被保存了,记不记得我们解析JSON时经常需要传递一个TypeReference的匿名子类?

Java提供了5种类型来支持泛型的泛型,类型之间的关系如下图

我们来看一下每种实现类分别代表着哪种类型的数据

类型

说明

举例

Class

具体类型

KeyNiuTech.class、Company.class

TypeVariable

类型变量

T extends Comparable

WildcardType

通配符

? super T

ParameterizedType

泛型类

Company、Comparable

GenericeArrayType

泛型数组

Company.staff通过Field获取后,读getGenericeType()返回的就是这个类型

最后一个示例结尾,这段代码用于打印类的定义信息

private static StringBuilder classDetail(Class clazz) {
    StringBuilder sb = new StringBuilder();

    int modifier = clazz.getModifiers();
    sb.append(Modifier.toString(modifier)).append(" ");
    sb.append(clazz.getName());

    TypeVariable[] tvs = clazz.getTypeParameters();
    if (tvs != null && tvs.length > 0) {
        sb.append("<");
        for (TypeVariable tv : tvs) {
            sb.append(tv.getName());
            Type[] bounds = tv.getBounds();
            if (bounds != null && bounds.length > 0) {
                sb.append(" extends ");
                for (Type bound : bounds) {
                    sb.append(bound.getTypeName());
                }
            }
        }
        sb.append(">");
    }

    Type superClass = clazz.getGenericSuperclass();
    if (superClass instanceof Class) {
        sb.append(" extends ");
        sb.append(superClass.getTypeName());
    } else if (superClass instanceof ParameterizedType) {
        ParameterizedType pt = (ParameterizedType) superClass;
        sb.append(" extends ");
        sb.append(pt.getRawType().getTypeName());
        sb.append("<");
        Type[] actualTypes = pt.getActualTypeArguments();
        for (Type at : actualTypes) {
            sb.append(at.getTypeName());
        }
        sb.append(">");
    }

    Type[] superInterfaces = clazz.getGenericInterfaces();
    for (Type ifs : superInterfaces) {
    }

    return sb;
}

输出示例

public static com.company.generic.GenericReflect$KeyNiuTech extends com.company.generic.GenericReflect$Company<java.util.Date>

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值