java的泛型解析以及相关知识点

一、为何引入泛型

    在java增加泛型之前,这种类似的功能是通过维护Object数组实现的,这种方法有两个问题,一是当获取的一个值的时候必须强制类型转换,二是没有错误检查可以向数组内添加任何类的对象。同时一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类,如果要编写可以应用于多种类型的代码,这种限制对代码的束缚就会很大。但是编码者希望编写的是更加通用化的代码,使代码能够应用于某种不确定的类型,而不是一个具体的接口或者一个类。

二、泛型优点

    所以在JDK1.5的时候才增加了泛型,出现了泛型的概念。即泛型实现了参数化类型的概念,使代码可以应用于多种类型。泛型的意思是:适用于很多很多的类型。并且最初的目的是为了让类或方法能够具备更广泛的表达能力。比如可以构建一个可以应用于各种类型的对象的工具。而且引入泛型的目的也和语法糖有关,因为泛型本身就是一种语法糖。泛型作为语法糖的好处就是安全简单,通过泛型使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现ClassCastException(类型转换异常)。这种语法能让编译器捕获大量不安全的代码,会保证类型错误会在编译期被发现而不是运行时,这就是编译器来保证类型的正确性也是静态类语言的好处,使用编译时信息协助排除大量运行时问题。

    因为适用于多种类型也方便了容器的使用,使其能记住元素的数据类型,这也是泛型最重要的一点,通过它来指定容器要持有什么类型。在刚开始的时候集合里的类型都是Object类型的,这样的好处就是可以自由的放入以及取出,但是当一个对象放入集合时,集合不会记住此对象的类型,再次从集合中取出该对象时直接会把该对象的编译类型变成Object,所以想要使用正确类型的对象就要人为的进行强制类型转换。通过泛型的使用就可以避免手动进行强制类型转换。

    泛型的实质就是允许在定义接口、类时,声明类型形参,这个形参在接口或者类内都被当成类型使用,可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,这样就可以暂时不指定类型,而是稍后在决定具体使用什么类型。当在使用这个类时,再用实际的类型替换此类型。但在另一种情况下,在接口或者类中没有使用泛型,定义方法时想定义类型形参就可以使用泛型方法。这里面有局限性,基本类型无法作为类型参数。

    这也是java泛型的核心:告诉编译器想使用什么类型,然后编译器帮你处理细节。

    总结以上泛型的优势是:

    1.类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。

    2.消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。

    3.泛型做为语法糖方便了编码者的使用,同时方便了容器的使用

    4.泛型优化了java面向对象的使用,可以将一段代码用于不同的类

    5.潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换插入生成的字节码中。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

三、泛型的相关知识点

    1.类型参数

    泛型的根本语法是<T>,这个语法有个专门的名称——类型参数,泛型还有一个名称——参数化类型,这代表容易由其他类型参数化,通过在使用/调用时传入具体的类型(类型实参)时,就为类型参数指定了具体的值。定义由参数的类型时,要使用一种不对类型参处做任何假设的方式指定具体的值,所以使用了类型参数E作为占位符。类型参数E即可以作返回类型的参数,也可以作方法参数类型的参数。

    在java库中使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T表示任意类型。

    //定义接口时指定了一个类型形参,该形参名为E

    public interface List<E> extends Collection<E> {

        //在该接口里,E可以作为类型使用

        public E get(int index) {} public void add(E e) {}

    }

    //定义类时指定了一个类型形参,该形参名为E

    public class ArrayList<E> extends AbstractList<E> implements List <E> {

        //在该类里,E可以作为类型使用

        public void set(E e) { .......................

        }

    }

    //接口或者类中没有使用泛型,定义方法时想定义类型形参就可以使用泛型方法

    public class Main{

        public static <T> void out(T t){

            System.out.println(t);

        }

        public static void main(String[] args){

        }

    }

    从JDK 1.5之后,java就允许定义泛型类、泛型接口、泛型方法。

    2.泛型类

    泛型类可以看作普通类的工厂,定义一个容器类,存放键值对key-value,通过泛型可以使键值对的类型先不确定,分别指定为K和V。只需要在使用该类时指定K、V的具体类型即可,这样可以创建含有不同数据类型的实例用来存放不同的数据类型。

    之后在jdk1.7增加了泛型的语法:允许在构造器后只给出一对<>即可,java可以通过参数推断<>里是什么类型。然后只能在该对象中存入该类型的对象,同时取出的也是对应的类型。通过这个菱形语法可以省略重复的类型值

    Classname<String,Integer> c2=new Classname<>("age",22);

    当泛型类派生子类,当创建了带泛型声明的接口、父类,就可以为接口和类创建实现类或派生子类,但是在使用这些接口、父类派生的子类的时候不能在包含类型形参需要传入具体的类型而不是通过字母替代。如果不进行指定就会被系统默认为Object类型。

    (1)错误的方式:public class A extends Container<K, V>{}

    (2)正确的方式:public class A extends Container<Integer, String>{}

    3.泛型接口

    接口使用泛型和类使用泛型没什么区别,称为泛型接口,为了区别类与接口在接口前加上字母I。对于使用了泛型的接口子类会有两种实现方式:

    (1)在子类继续设置泛型标记 Class xxx <S> implements 接口名 <S>

    (2)在子类不设置泛型,而为父接口明确的定义一个泛型类型 class xxx implements 接口名<具体泛型类型>

    不管原本之间两个类S和T之间有什么联系,通常Pair<S>与Pair<T>是没什么联系的,因为在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型。但是泛型类可以扩展或实现其他的泛型类比如ArrayList<Manager>可以被转换为一个List<Manager>,就如前面所说ArrayList<Manager>不是一个ArrayList<Employee>或List<Employee>。因为泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

    4.泛型方法

    所谓泛型方法就是在声明方法的时候定义一个或多个类型形参,泛型方法会根据实际传入的对象,编译器就会判断出类型形参T所代表的实际类型,这就是类型参数推断,但是类型推断只对赋值操作有效其他时候并不起作用,用法格式如下:

    修饰符<T, S> 返回值类型 方法名 (形参列表){

        方法体

    }

    如果将一个泛型方法调用的结果作为参数传递给另一个方法,这时编译器不会执行类型推断,因为这个时候编译器会认为调用泛型方法后其返回值被赋给一个Object类型的变量。这样是会导致不能进行编译,如果要进行这种操作只能显示的指明类型,要在点操作符和方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类内,点操作符之前使用this。如果使用static方法必须在点操作符之前加上类名。

    方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参可以在整个接口、类中使用。泛型方法使得该方法能够独立于类而产生变化。

    5.泛型构造器

    java也允许在构造器签名中声明类型形参,就是泛型构造器。使用普通泛型方法一样,一种是显试指定泛型参数,另一种是隐式推断。如果是显示指定则以显式指定的类型参数为准,如果传入的参数的类型和指定的类 型实参不符,将会编译报错。以下是实例:

    public class Person {

         public <T> Person(T t) {

             System.out.println(t);

        }

    }

    public static void main(String[] args){

        //隐式

        new Person(22);

        //显示

        new<String> Person("hello");

    }

     一个类的构造器是泛型构造器,同时该类还是泛型类的时候,泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这时前后的类型需要统一,如果不同,Person<String> a = new <Integer>Person<>(15); 这种语法不允许,会直接编译报错。但是通过编译器能推导出类型参数的值,通过

    6.类型通配符

    泛型技术虽然解决了向下转型带来的安全隐患问题,但是又出现了一个新的问题,即便是同一个类由于设置泛型类型不同其对象表示的含义也不同,因此不能够直接进行引用操作。虽然都是一个类的对象但是对象之间不能够进行直接的引用传递操作,这样会在方法参数传递上造成新的问题,此时可以利用通配符"?"来进行描述。采用了通配符作为使用的泛型类型,只要是一个类的对象不管何种泛型类型方法都可以接受。

    类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参,或者编译时不知道要使用什么类型的时候,类型通配符是一个问号?,在使用的时候可以匹配任何类型。如果想让容器的类型具有父子关系就需要使用未知类型,使用通配符的目的是限制泛型的类型参数的类型,使其满足某种条件固定为某些类,通配符不是类型变量所以不能在代码中使用?作为一种类型。这样就衍生出了上限通配符和下限通配符。这就是受限通配符,也就是类型参数约束条件。通配泛型类型有三种形式——?、? extends、?super

    (1)上限通配符

    代表类型协变,表示容器类型之间和负载类型之间具有相同的关系。

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

    List<? extends Shape>表示该集合内的元素都要是Shape类型或其子类。在进行实例化时,指定类型实参 只能是extends后类型的子类或其本身。Circle是其子类:List<? extends Shape> list = new ArrayList<Circle>();

    (2)下限通配符

    代表类型逆变,表示容器类型之间和负载类型之间具有相反的关系

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

    List <? super Circle>表示该集合中的所有元素都是Circle类型或者其父类。在进行例化时,指定类型实参只 能是extends后类型的子类或其本身。Shape是其父类:List<? super Circle> list = new ArrayList<Shape>();

    通过限制来保证在使用一些方法时,对应的类实现了相关方法。

    就是说list要是对外生产元素,也就是get元素,用? extends,如果是消费元素,也就是set元素,用?super,单独用?类型不安全,get之后只能当object用

    通配符的意义在于可以接收类对象,但是不能修改对象属性,设置一个类为泛型类时没有继承的概念,也就是Object与String在类定义里属于父类和子类的关系但是换到泛型里就属于两个完全独立的概念。如果不使用通配符就会导致在方法里可以随意修改对象内容,通配符设置的类型只表示可以取出但是不能设置,一旦设置程序编译就会出现错误。

    7.通配符捕获

    (1)通配符捕获

    展示了一些错误代码,但实际上不能。已下代码意为:包含一个泛型 Box、提取它的值并试图将值放回同一个 Box。

    public void rebox(Box<?> box) {

        box.put(box.get());

    }

    Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied to (java.lang.Object) box.put(box.get()); 1 error

    这个代码看起来应该可以工作,因为取出值的类型符合放回值的类型,但是,编译器生成关于 “capture#337 of ?” 与 Object 不兼容的错误消息。

    “capture#337 of ?” :当编译器遇到一个在其类型中带有通配符的变量,比如 rebox() 的 box 参数,它认识到必然有一些 T ,对这些 T 而言 box 是 Box<T>。它不知道 T 代表什么类型,但它可以为该类型创建一个占位符来指代 T 的类型。占位符被称为这个特殊通配符的捕获。这种情况下,编译器将名称 “capture#337 of ?” 以 box 类型分配给通配符。每个变量声明中每出现一个通配符都将获得一个不同的捕获,因此在泛型声明 foo(Pair<?,?> x, Pair<?,?> y) 中,编译器将给每四个通配符的捕获分配一个不同的名称,因为任意未知的类型参数之间没有关系。

    错误消息告诉我们不能调用 put(),因为它不能检验 put() 的实参类型与其形参类型是否兼容 — 因为形参的类型是未知的。在这种情况下,由于 ? 实际表示 “?extends Object” ,编译器已经推断出 box.get() 的类型是 Object,而不是 “capture#337 of ?”。它不能静态地检验对由占位符 “capture#337 of ?” 所识别的类型而言 Object 是否是一个可接受的值。

    (2)捕获助手

    虽然编译器似乎丢弃了一些有用的信息,我们可以使用一个技巧来使编译器重构这些信息,即对未知的通配符类型命名。通过一个泛型辅助方法reboxHelper来实现rebox() ,以下为 “捕获助手” 方法:

     public void rebox(Box<?> box) {

         reboxHelper(box);

    }

    private<V> void reboxHelper(Box<V> box) {

        box.put(box.get());

    }

    助手方法 reboxHelper() 是一个泛型方法,泛型方法引入了额外的类型参数,参数用于表示参数和/或方法的返回值之间的类型约束。然而就 reboxHelper() 来说,泛型方法并不使用类型参数指定类型约束,它允许编译器(通过类型接口)对 box 类型的类型参数命名。

    捕获助手技巧允许我们在处理通配符时绕开编译器的限制。当 rebox() 调用 reboxHelper() 时,它知道这么做是安全的,因为它自身的 box 参数对一些未知的 T 而言一定是 Box<T>。因为类型参数 V 被引入到方法签名中并且没有绑定到其他任何类型参数,它也可以表示任何未知类型,因此,某些未知 T 的 Box<T> 也可能是某些未知 V 的 Box<V>。现在 reboxHelper() 中的表达式 box.get() 不再具有 Object 类型,它具有 V 类型 — 并允许将 V 传递给 Box<V>.put()。

    不将 rebox() 声明为一个泛型方法的主要原因是:如果一个类型参数在方法签名中只出现一次,它很有可能是一个通配符而不是一个命名的类型参数。一般来说,带有通配符的 API 比带有泛型方法的 API 更简单,在更复杂的方法声明中类型名称的增多会降低声明的可读性。因为在需要时始终可以通过专有的捕获助手恢复名称,这个方法让您能够保持 API 整洁,同时不会删除有用的信息。

    (3)类型推断

    捕获助手技巧涉及多个因素:类型推断和捕获转换。Java 编译器在很多情况下都不能执行类型推断,但是可以为泛型方法推断类型参数(其他语言更加依赖类型推断,将来我们可以看到 Java 语言中会添加更多的类型推断特性)。如果愿意,您可以指定类型参数的值,但只有当您能够命名该类型时才可以这样做 — 并且不能够表示捕获类型。因此要使用这种技巧,要求编译器能够为您推断类型。捕获转换允许编译器为已捕获的通配符产生一个占位符类型名,以便对它进行类型推断。当解析一个泛型方法的调用时,编译器将设法推断类型参数它能达到的最具体类型。 例如,对于下面这个泛型方法:

    public static<T> T identity(T arg) { return arg };

    和它的调用:

    Integer i = 3;

    System.out.println(identity(i));

    编译器能够推断 T 是 Integer、Number、 Serializable 或 Object,但它选择 Integer 作为满足约束的最具体类型。当构造泛型实例时,可以使用类型推断减少冗余。例如,使用 Box 类创建 Box<String> 要求您指定两次类型参数 String:

    Box<String> box = new BoxImpl<String>();

    即使可以使用 IDE 执行一些工作,也不要违背 DRY(Don't Repeat Yourself)原则。然而,如果实现类 BoxImpl 提供一个类似清单 5 的泛型工厂方法(这始终是个好主意),则可以减少客户机代码的冗余:

    8.类型擦除

    其实在引入泛型之后就出现了一个问题,如何让类型系统既能使用旧的非泛型集合又可以使用新的泛型集合类,可以通过如下代码完成:

    List someThings = getSomeThings()

    List<String> myString = (List<String>)someThings;

    虽然这种做法并不安全,但实现了兼容。这种兼容也是类型擦除带来的好处,通过类型擦除实现这种兼容性。因为javac会去掉类型参数。非泛型的List一般叫原始类型。也就是在进行擦除之后就都会变成List、Map等这样的容器的原始类型。

    不管泛型的类型形参传入哪一种类型实参,都会被当成同一类来处理,在内存中也只占用一块内存空间,java泛型这一概念只是作用于代码编译阶段,所以在泛型代码内部无法获得任何有关泛型参数类型的信息,在编译过程中对于正确检验泛型结果后会将泛型的信息擦除,在编译之后的class文件中是不包含任何泛型信息的,这些信息不会进入到运行时阶段。所以当泛型作为重载的条件时要注意,会在运行时无法通过签名来区分。所以在一些情况下类型擦除会与多态发生冲突,解决这个问题的方法是编译器会在发生冲突的类里生成一个桥方法,虚拟机用引用的对象调用该方法,这个方法传入的参数是Object类型的,在这个方法的内部会将传进来的对象强制转换为擦除之前的类型。将会产生相同的效果。

    所以java泛型是通过类型擦除来实现的,在使用泛型的时候,任何具体的类型信息都被擦除了,在下层唯一知道的就是使用了一个对象,这些类型都被擦除成原生类型,原始类型就是擦除了泛型信息,最后在字节码中的类型变量的真正类型,也就是删去类型参数后的泛型类型名。无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。一般原始类型用第一个限定的类型变量来替换。

    例如:

    public static <T extends Comparable>T min(T[] a)

    类型擦除之后,就如上所说留下限定的类型来替换:

    public static Comparable min (Comparable[] a)

   在调用泛型方法的表达式中会在擦除返回类型后编译器会自动插入强制类型转换,编译器会把这个过程分成两条虚拟机指令:

    (1)对原始方法的调用

    (2)将返回的Object类型强制转换为对应的类型

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

    9.元组概念

仅用一次方法调用,就能返回多个对象,但是return语句只能返回单个对象,所以解决办法就是创建一个对象,这样就可以用它来持有想要返回的多个对象,这就是元组的概念,将一组对象直接打包进一个单一对象,这个容器对象允许读取但是不允许存放新的元素。通过泛型就可以避免在每次要使用的时候专门创建一个类来完成工作。使用元组只需要定义一个长度合适的元组,作为方法的返回值在return语句创建该元组。

四、java泛型转换的事实

    1.虚拟机中没有泛型,只有普通的类和方法

    2.所有类型参数都用它们的限定类型替换

    3.通过桥方法合成来保持多态

    4.为保持类型安全型,必要时插入强制类型转换

    5.不能用基本类型实例化类型参数,类型擦除后会转换成object,但是object是不能存储基本类型的

    6.不能创建参数化类型数组,因为数组协变导致可以接受子类型的类型擦除转化为object类型之后,就可以存储其他类型的元素了,这样使用泛型的意义就没有了,因为不能不能检查出类型相关的错误。但是可以进行声明Pair<String>[],如果想安全的收集参数化类型对象可以使用ArrayList<Pair<String>>

    7.不能实例化类型变量 first = new T(); 解决办法有两个:

    (1)可以通过让使用者提供一个构造器表达式 pair<String> p =Pair.makePair(String::new);然后通过方法的函数式接口Supplier<T>表示一个返回类型为T的函数。

    public static <T> Pair<T> makePair(Supplier<T> conster){

        return new Pair<>(constr.get(),constr.get());

    }

    (2)也可以通过反射调用Class.newInstance方法来构造泛型对象,不能通过T.class,必须进行如下:

    public static <T> Pair<T> makePair(Class<T> cl){

        try{ return new Pair<>(cl.newInstance(),cl.newInstance());}

        catch (Exception ex){return null;}

    }

    在用如下方法调用 Pair<String> p=Pair.makePair(String.class)

    8.也不能构造泛型数组T[] n= new T[2]; ,因为类型擦除会让这个数组的类型永远构造成限定类型

    9.不能使用带有类型变量的静态域和方法

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/104147134