Java泛型详解

Java泛型

个人技术博客(IBLi)
CSDN Github 掘金

1、泛型定义

使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后在进行强制类型转换的代码具有更好的安全性和可读性。 --《Java核心技术》

泛型是在编译时期作用的;

泛型变量使用大写形式,在Java库中,一般使用变量E表示集合的元素类型,K和V表示表的关键字与值的类型。

2、通配符

2.1 无边界通配符

无边界通配符又成为非限定通配符

public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        list1.add("1");
        list1.add("2");
        list1.add("3");
        list1.add("4");
        loop(list1);
    }

    public static void loop(List<?> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

2.2 上边界通配符

上边界通配符和下边界通配符都属于限定通配符

public static void main(String[] args) {
        //List中的类型必须是Number的子类,不然会报编译错误
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        list1.add(4);
        loop(list1);
    }

    // 传进来的list的类型必须是Number或Number的子类才可以
    public static void loop(List<? extends Number> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

? extends Number
如果限定的类型有多个,之间使用 & 进行分割

2.3 下边界通配符

public static void main(String[] args) {
        //List的泛型是Number 添加的元素只要是Number下的类型就可以
        List<Number> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2L);
        list1.add(new BigDecimal(22));
        list1.add(4);
        loop(list1);
    }

    /**
     * 通用类型必须是Number到Object之间的类型
     *
     * @param list
     */
    public static void loop(List<? super Number> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

3、泛型的使用

泛型必须先声明,再使用,不然会有编译错误;
泛型的声明是用过一对<>来完成,约定使用一个大写的字母来表示;
通配符不能用作返回值;

public <T> T testA(T t, Test1<T> test1) {
    System.out.println("这是传入的T:" + t);
    t = test1.t;
    System.out.println("这是赋值后的T:" + t);
    return t;
}
  • 要从泛型类取数据时,用extends;
  • 要往泛型类写数据时,用super;
  • 既要取又要写,就不用通配符(即extends与super都不用)。

3.1 泛型类

public class Demo<K, V> {
    public <K> K test(V v) {
        return null;
    }
}

3.2 泛型方法

public class DemoTest4<K, V> {

    /**
     * <T> 代表泛型的声明
     *
     * @param t   本方法声明的泛型类型
     * @param <T> 本方法声明的泛型类型
     * @return
     */
    public <T> T test(T t) {
        return null;
    }

    /**
     * 普通的泛型方法
     *
     * @param k   类中定义的泛型类型
     * @param <X> 本方法中声明的泛型类型
     * @return
     */
    public <X> X aa(K k) {
        return (X) null;
    }

    /**
     * 静态方法中是无法使用类中声明的泛型类型的
     * 可以使用在本方法中声明的泛型类型
     *
     * @return
     */
    public static <X> X bb() {
        return null;
    }

}

3.3 泛型接口

首先看一下不使用泛型接口的Demo

先定义接口,声明两个方法
public interface IGeneric {
    Integer aa(Integer a);
    
    Integer bb(Integer b);
}

然后创建一个类来实现方法:
public class IntegerDemo implements IGeneric{

    @Override
    public Integer aa(Integer a) {
        return null;
    }

    @Override
    public Integer bb(Integer b) {
        return null;
    }

}

上面是没有使用泛型的接口设计,但是aa方法的操作类型相当于在接口中写死了,如果此时我们需要一个String类型的aa方法,那是不是还要在声明一个String类型的接口,然后再去实现呢,这样是不是显得代码很臃肿,代码重复;
所以我们可以看一下使用泛型之后是怎么样的。

定义泛型接口
public interface IGenericInte<T> {
    T aa(T a);
    T bb(T b);
}

下面是根据不同类型的实现类
泛型传如Integer类型
public class IGenericInteger implements IGenericInte<Integer> {
    @Override
    public Integer aa(Integer a) {
        return null;
    }

    @Override
    public Integer bb(Integer b) {
        return null;
    }
}

泛型传入String类型
public class IGenericString implements IGenericInte<String> {
    @Override
    public String aa(String a) {
        return null;
    }

    @Override
    public String bb(String b) {
        return null;
    }
}

4、泛型擦除

在虚拟机上没有泛型类型对象,所有的对象都属于普通类。Java在处理泛型类型的时候,会处理成一个相应的原始类型。 擦除类型变量,并替换为限定类型,如果没有限定类型,默认使用Object替代。如果有限定类型,并且是多个,会使用第一个限定的类型来替换。

public interface IGenericInte<T> {
    T aa(T a);
    T bb(T b);
}

像上面这个T是一个无限定的变量,泛型擦除之后会直接使用Object替换。
当然调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换

Pair<Employee> buddies = ....
Employee buddy = buddies.getFirst();

擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换,也就是说,编译器调用方法是其实是执行了一下两个虚拟机指令:

  • 对原始方法Pair.getFirst()方法的调用
  • 将返回的Object类型强制转换为Employee类型
public static <T extends Comparable> T foo(T [] args)

在擦除类型之后变成:

public static Comparable T foo(Comparable [] args)

参数类型T已经被擦除,只留下限定类型Comparable;

总之有关Java泛型转换的事实:

  • 虚拟机没有泛型,只有普通的类和方法
  • 所有的类型参数都用它们的限定类型替换
  • 桥方法被合成来保证多态
  • 为了保持类型安全型,必要时插入强制类型转换

第一条应该很好理解,这也是为什么会有泛型擦除这个概念,是因为JVM不能操作泛型;
第二条就是解释泛型如何进行类型的擦除;
第三条是泛型方法可能与多态的理念矛盾,所以使用桥方法来过渡或兼容;
第四条上面也有提到,会出现强制类型转换的情况;

5、泛型的约束与局限性

当然泛型的设计在java中并没有那么完美,它确实可以解决代码结构重用等问题,但是也是有一些局限性,下面是我根据《Java核心技术》进行的总结:

5.1 不能使用基础数据类型实例化类型参数

原因是类型擦除之后,如果使用Object原始类型,Object是无法存储基本数据类型的值。所以只能通过其包装类型声明;

5.2 运行时查询类型只适用与原始类型

public class DemoTest5<T> {
    public static void main(String[] args) {
        DemoTest5<String> demoTest5 = new DemoTest5<>();
        DemoTest5<Integer> demoTest4 = new DemoTest5<>();
        System.err.println(demoTest4.getClass().equals(demoTest5.getClass()));
    }
}

demoTest4.getClass().equals(demoTest5.getClass())其实比较的是DemoTest5这个类类型,我们输出一下demoTest4.getClass()的结果看一下:

class com.ibli.javaBase.generics.DemoTest5

所以这里有一道非常经典的面试题,如何判断一个泛型他的具体类型是什么,这里我们可以使用反射去拿到泛型的具体类型;

5.3 不能创造参数化类型的数组

对于参数化类型的数组,在类型擦除之后,会变成Object[]类型,如果此时试图存储一个String类型的元素,就会抛出一个Array-StoreException异常;
主要目的还是处于到数组安全的保护,可以参考几篇文章:

1、如果Java不支持参数化类型数组,那么Arrays.asList()如何处理它们?
2、java不能创建参数化类型的泛型数组
3、java.lang.ArrayStoreException

5.4 Varargs警告

向参数个数可变的方法传递一个泛型类型的实例的场景,编译器会发出警告!
抑制这种警告的方式有两种:

  • 在调用方法上增加注解@SuppressWarnings(“unchecked”)
  • 还可以使用@SafeVarargs注解直接标注方法

参考 java不能创建参数化类型的泛型数组

5.5 不能实例化类型变量

不能使用new T(…) 或则new T[…]和T.class这样的表达式的类型变量;因为类型擦除后,T变成Object,显然我们在这里并不是想要创建一个Object实例。解决办法是在调用者提供一个构造器表达式,下面是用Supplier函数实现:

public class Pair<T> {

        private T first;
        private T second;

        public T getFirst() {
            return first;
        }

        public void setFirst(T first) {
            this.first = first;
        }

        public T getSecond() {
            return second;
        }

        public void setSecond(T second) {
            this.second = second;
        }

        public Pair(T first, T second) {
            this.first = first;
            this.second = second;
        }

        public static <T> Pair<T> build(Supplier<T> constr) {
            return new Pair<>(constr.get(), constr.get());
        }

        /**
         * Cannot infer type arguments for Pair2<>
         * 当函数头返回值为Pair时,无法推断,改为Pair2后可以推断.
         * @param c1
         * @return
         */
        public static <T> Pair<T> build(Class<T> c1){
            try {
                return new Pair<>(c1.newInstance(),c1.newInstance());
            } catch (InstantiationException | IllegalAccessException e) {
                return null;
            }
        }
}

Supplier是一个函数接口,返回一个无参数并且返回类型为T的函数:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
public class TestMakePair {

    public static void main(String[] args) {

        /**
         * 1.接受Supplier<T>--它是一个函数式接口。表示无参数且返回类型为T的函数。
         * 因为不能实例化类型变量,如:
         * public Pair() {first = new T();second = new T();}
         * 所以最好的方式是让调用者提供一个构造器表达式.形式如下:
         * @param constr
         * @return
         */
        Pair<String> pair = Pair.build(String::new);
        System.out.println(pair.getFirst().length());

        /**
         * public void buildT(){
         2.传统的方式是通过Class.newInstance方法来构造泛型对象.
         但由于细节过于复杂,T.class是不合法的.它会被擦除为Object.class.如下:
         Illegal class literal for the type parameter T
         T.class.newInstance();
         }
         * 3.
         * T.class是不合法的,但若API涉及如下
         * reason:因为String.class是Class<String>的一个实例.
         */
        Pair<String> pair1 = Pair.build(String.class);
        System.out.println(pair1.getFirst().length());
    }
}

执行结果:
0
0

5.6 不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组。数组本身也有类型,用来监控存储在JVM中的数组,这个类型会被擦除,例如:

public static <T extends Comparable> T[] foo(T[] a){
    T[] mm = new T[2];
    ...
}

类型擦除,会让这个方法永远构造Comparabel[2]数组;

5.7 泛型类的静态上下文中类型变量无效

这个应该是比较好理解的,上文也提到过了,泛型类型是作用在泛型类上的,一些静态的方法或这静态的属性不能够使用泛型类的变量类型,编译器会直接报错;

5.8 不能抛出或者捕获泛型类的实例

Java既不能抛出也不能捕获泛型类对象,实际上,甚至泛型类扩展Throwable都是不合法的。

public static <T extends Throwable> void doWork(Class<T> t){
    try{
        ...
    }catch (T ex){  此处无法捕获    catch必须捕获具体的异常
        ....
    }
}

在异常规范中使用类型变量是允许的,如下:

public static <T extends Throwable> void doWork(Class<T> t) throws T {
  try{
        ...
    }catch (Throwable ex){  
        t.initCause(ex);
        throw t;
    }
}

5.9 可以消除对受查异常的检查

Java异常处理要求必须为所有的受查异常提供一个处理器,但是使用泛型,可以规避这一点;

@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T{
    throw (T)e;
}

调用上面的方法,编译器会认为t是一个非受查异常;

5.10 注意擦除后的冲突

比如一个泛型类的equals方法,擦除之后,和Object的equals冲突;解决办法是重新命名引发错误的方法;

6、泛型的继承关系

如果Manage extends Employee,那么Pair是Pair的子类吗? 不是的!
但是泛型类可以扩展或实现其他的泛型类,很典型的一个例子ArrayList:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

ArrayList[E]继承了AbstractList[E];

对于Java泛型的一些思考

编译器如何推断出具体的类型? 参考资料:深入理解 Java 泛型

------------------- 他日若遂凌云志 敢笑黄巢不丈夫 -------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值