美文网首页
Java 中的泛型

Java 中的泛型

作者: wuchao226 | 来源:发表于2020-12-31 14:30 被阅读0次

    泛型的目的:Java 泛型就是把一种语法糖,通过泛型使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现 ClassCastException,即类型转换异常。

    泛型的好处

    • 类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性。
    • 消除了代码中许多的强制类型转换,增强了代码的可读性。
    • 为较大的优化带来了可能。

    为什么需要泛型

    通过下面的代码就可知道原因

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

    开发中经常有数值类型求和的需求,例如实现 int 类型的加法,有时还需要 long 类型的求和,如果还需要 double 类型的求和,需要重新在重载一个输入是 double 类型的 add 方法。

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("corn");
        list.add("qq");
        list.add(22);
        for (int i = 0; i < list.size(); i++) {
             String name = (String) list.get(i);
             System.out.println("args = " + name);
        }
    }
    
    图一

    定义一个 List 类型的集合,先向其中加入了两个字符串类型的值,随后加入一个 Integer 类型的值。这是完全允许的,因为此时 lis t默认的类型为 Object 类型。在之后的循环中,由于忘记了之前在 list 中也加入了 Integer 类型的值或其他编码原因,很容易出现类似于图一中的错误。因为编译阶段正常,而运行时会出现java.lang.ClassCastException异常。因此,导致此类错误编码过程中不易发现。

    在如上的编码过程中,我们发现主要存在两个问题:

    1. 当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了 Object 类型,但其运行时类型任然为其本身类型。
    2. 因此,String name = (String) list.get(i); 处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。

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

    泛型,即“参数化类型”,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用或调用时传入具体的类型(类型实参)。

    泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

    泛型类

    引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用 <> 括起来,并放在类名的后面。泛型类是允许有多个类型变量的。

    public class NormalGeneric<T> {
        private T data;
    
        public NormalGeneric() {
        }
    
        public NormalGeneric(T data) {
            this.data = data;
        }
    
        public T getData() {
            return data;
        }
    }
    
    public class NormalGeneric2<T,K> {
        private T data;
        private K result;
    
        public NormalGeneric2() {
        }
    
        public NormalGeneric2(T data, K result) {
            this.data = data;
            this.result = result;
        }
    }
    
    泛型接口

    泛型接口与泛型类的定义基本相同。

    public interface Generator<T> {
        void next(T t);
    }
    

    而实现泛型接口的类,有两种实现方法:
    1、未传入泛型实参时:

    public class ImplGenerator<T> implements Generator<T> {
        @Override
        public void next(T t) {
            System.out.println("show:" + t);
        }
    }
    

    在 new 出类的实例时,需要指定具体类型:

    public static void main(String[] args) {
       ImplGenerator<String> implGenerator = new ImplGenerator<>();
       implGenerator.next("9");
    }
    

    2、传入泛型实参

    public class ImplGenerator2 implements Generator<String> {
        @Override
        public void next(String t) {
            System.out.println("show:" + t);
        }
    }
    

    在 new 出类的实例时,和普通的类没区别。

    泛型方法
    public class GenericMethod {
    
        /**
         * public:修饰符
         * <T>:返回值
         */
        public <T> T genericMethod(T... a) {
            return a[a.length / 2];
        }
    
        public void test(int x, int y) {
            System.out.println(x + y);
        }
    
        public static void main(String[] args) {
            GenericMethod genericMethod = new GenericMethod();
            genericMethod.test(5, 6);
            System.out.println(genericMethod.genericMethod("a", "b", "c"));
            System.out.println(genericMethod.genericMethod("sdf", "fdg ", 3));
        }
    }
    

    泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。

    普通方法:

     public class Generic<T>{
            private T key;
    
            public Generic(T key) {
                this.key = key;
            }
    
            //虽然在方法中使用了泛型,但是这并不是一个泛型方法。
            //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
            //所以在这个方法中才可以继续使用 T 这个泛型。
            public T getKey() {
                return key;
            }
    
            /**
             * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
             * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
             */
            public E setKey(E key){
                this.key=key;
            }
        }
    

    泛型方法

        /**
         * 这才是一个真正的泛型方法。
         * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
         * 这个T可以出现在这个泛型方法的任意位置.
         * 泛型的数量也可以为任意多个
         */
        public <T> T showKeyName(Generic<T> container) {
            System.out.println("container key:" + container.getKey());
            T test = container.getKey();
            return test;
        }
    
       //这也不是一个泛型方法,这就是一个普通的方法,
        // 只是使用了Generic<Number>这个泛型类做形参而已。
        public void show(Generic<Number> obj){
    
        }
    
        /**
         * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
         * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
         * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
         */
    //    public <T,E> T show(E ab){
    //        //
    //    }
    
        /**
         * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
         * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
         * 所以这也不是一个正确的泛型方法声明。
         }
         */
    //    public void show(T obj){
    //
    //    }
    
    

    限定类型变量

    我们需要对类型变量加以约束,比如计算两个变量的最小,最大值。

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

    如何确保传入的两个变量一定有 compareTo 方法?

    解决方案就是将 T 限制为实现了接口 Comparable 的类。

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

    T extends Comparable 中,T 表示应该绑定类型的子类型,Comparable 表示绑定类型,子类型和绑定类型可以是类也可以是接口。

    同时 extends 左右都允许有多个,如 T,V extends Comparable & Serializable。
    注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。
    这种类的限定既可以用在泛型方法上也可以用在泛型类上

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

    泛型中的约束和局限性

    泛型类

    public class Restrict<T> {}
    
    不能用基本类型实例化类型参数
    // Restrict<double> 这种不允许
    Restrict<Double> restrict = new Restrict<>();
    
    运行时类型查询只适用于原始类型
    // if (restrict instanceof Restrict<Double>){} 这种不允许
    // if (restrict instanceof Restrict<T>){} 这种不允许
    System.out.println(restrict.getClass() == restrictString.getClass());
    System.out.println(restrict.getClass().getName());
    
    泛型类的静态上下文中类型变量失效
    // 静态域或者方法里不能引用类型变量
    // private static T instance;
    // 静态方法 本身是泛型方法就行
    private static <T> T getInstance(){};
    

    不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是 static 的部分,然后才是构造函数等等。所以在对象初始化之前 static 的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。

    不能创建参数化类型的数组
    Restrict<Double>[] restrictArray;// 可以
    // Restrict<Double>[] restricts = new Restrict<Double>[10]; // 不允许
    
    不能实例化类型变量
    // 不能实例化类型变量
    //    public Restrict(T data) {
    //        this.data = new T();
    //    }
    
    不能捕获泛型类的实例
    // 泛型类不能 extends Exception/Throwable
    private class Problem<T> extends Exception;
    
    // 不能捕获泛型类对象
    public <T extends Throwable> void doWork(T x) {
        try {
    
        } catch (T x) {
            // do sth
        }
    }
    

    但是这样可以:

    public <T extends Throwable> void doWorkSuccess(T x) throws T{
        try {
                
        }catch (Throwable e){
            throw x;
        }
    }
    

    泛型类型的继承规则

    有一个类和子类

    public class Employee {
    }
    
    public class Worker extends Employee {
    }
    

    有一个泛型类

    public class Pair<T> {
    }
    

    那么 Pair<Employee> 和 Pair<Worker> 是继承关系吗?
    答案:不是,他们之间没有什么关系。


    36DAEA90-C871-48C0-81C7-E63569FDC864.png

    但是泛型类可以继承或者扩展其他泛型类,比如 List 和 ArrayList。

    Pair<Employee> pair = new ExtendPair<>();
    
    // 泛型类可以继承或者扩展其他泛型类,比如 List 和 ArrayList
    private static class ExtendPair<T> extends Pair<T> {
    }
    

    通配符类型

    上述代码中 Pair<Employee> 和 Pair<Worker> 没有任何关系,如果我们有一个泛型类和一个方法

    public class GenericType<T> {
        private T data;
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    }
    
    private static void print(GenericType<Fruit> p) {
        System.out.println(p.getData().getColor());
    }
    

    继承关系的类

    public class Fruit {
    }
    public class Orange extends Fruit {
    }
    public class Apple extends Fruit{
    }
    public class HongFuShi extends Apple{
    }
    

    则会出现这种使用情况:

    通配符类型 ? 可以解决上述问题。
    有两种使用方式:

    • ? extends X 表示类型的上界,类型参数是X的子类
    • ? super X 表示类型的下界,类型参数是X的超类
    无限定的通配符 ?

    类型通配符是一个问号(?),将一个问号作为类型实参传给 List 集合,写作:List<?>(意思是元素类型未知的 List )。这个问号(?)被成为通配符,它的元素类型可以匹配任何类型。

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

    现在可以传入任何类型的List来调用test()方法,程序依然可以访问集合c中的元素,其类型是Object。

    List<?> c = new ArrayList<String>();
    //编译器报错
    c.add(new Object());
    

    但是并不能把元素加入到其中。因为程序无法确定c集合中元素的类型,所以不能向其添加对象。
    下面就该引入带限通配符,来确定集合元素中的类型。

    上限通配符 ?extends X

    如果想限制使用泛型类别时,只能用某个特定类型或者是其子类型才能实例化该类型时,可以在定义类型时,使用extends关键字指定这个类型必须是继承某个类,或者实现某个接口,也可以是这个类或接口本身。

    它表示集合中的所有元素都是Shape类型或者其子类
    List<? extends Shape>
    

    上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。

    ? extends X 表示传递给方法的参数,必须是 X 的子类(包括X本身)。如下代码所示:

    private static void print2(GenericType<? extends Fruit> p) {
        System.out.println(p.getData().getColor());
    }
    
    public static void main(String[] args) {
        GenericType<Fruit> genericType = new GenericType<>();
        print2(genericType);
    
        GenericType<Orange> genericType2 = new GenericType<>();
        print2(genericType2);
    
        GenericType<? extends Fruit> p = genericType;
    }
    

    但是对泛型类 GenericType 来说,如果其中提供了 get 和 set 类型参数变量的方法的话,set 方法是不允许被调用的,会出现编译错误。

    public class GenericType<T> {
        private T data;
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    }
    

    get 方法则没问题,会返回一个 Fruit 类型的值。

     GenericType<? extends Fruit> c = genericType;
     Fruit data = c.getData();
    

    原因: ? extends X 表示类型的上界,类型参数是 X 的子类,那么可以肯定的说,get 方法返回的一定是个 X(不管是X或者X的子类)编译器是可以确定知道的。但是 set 方法只知道传入的是个 X,至于具体是 X 的那个子类,不知道。

    总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。

    下限通配符 ?super X

    如果想限制使用泛型类别时,只能用某个特定类型或者是其父类型才能实例化该类型时,可以在定义类型时,使用super关键字指定这个类型必须是是某个类的父类,或者是某个接口的父接口,也可以是这个类或接口本身。

    它表示集合中的所有元素都是Circle类型或者其父类
    List <? super Circle>
    

    这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    例如:

    //Shape是其父类
    List<? super Circle> list = new ArrayList<Shape>();
    

    ?super X表示传递给方法的参数,必须是 X 的超类(包括X本身)

    对泛型类 GenericType 来说,如果其中提供了 get 和 set 类型参数变量的方法的话,set 方法可以被调用的,且能传入的参数只能是 X 或者 X 的子类

    public class GenericType<T> {
        private T data;
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    }
    

    get 方法只会返回一个 Object 类型的值。
    原因:? super X 表示类型的下界,类型参数是 X 的超类(包括 X 本身),那么可以肯定的说,get 方法返回的一定是个 X 的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以 get 方法返回Object。
    编译器是可以确定知道的。对于 set 方法来说,编译器不知道它需要的确切类型,但是 X 和 X 的子类可以安全的转型为 X。
    总结:主要用于安全地写入数据,可以写入 X 及其子类型。

    类型擦除

    Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

    如在代码中定义 List<Object> 和 List<String> 等类型,在编译后都会变成 List, JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况。

    ArrayList<String> list1 = new ArrayList<String>();
    list1.add("abc");
    
    ArrayList<Integer> list2 = new ArrayList<Integer>();
    list2.add(123);
    
    System.out.println(list1.getClass() == list2.getClass());
    

    程序输出:

    true

    在这个例子中,我们定义了两个 ArrayList 数组,不过一个是 ArrayList<String> 泛型类型的,只能存储字符串;一个是 ArrayList<Integer> 泛型类型的,只能存储整数,最后,我们通过 list1 对象和 list2 对象的 getClass() 方法获取他们的类的信息,最后发现结果为 true。说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型。

    类型擦除详细介绍

    在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。

    相关文章

      网友评论

          本文标题:Java 中的泛型

          本文链接:https://www.haomeiwen.com/subject/bwknnktx.html