带你了解编程语言Java的 泛型全解

    作者:课课家教育更新于: 2019-08-29 22:39:36

    大神带你学编程,欢迎选课

    聊一聊java 泛型全解,Java是一个面向对象的语言。对程序员来说,这意味着要注意应中的数据和操纵数据的方法(method),而不是严格地用过程来思考。在一个面向对象的系统中,类(class)是数据和操作数据的方法的集合。数据和方法一起描述对象(object)的状态和行为。每一对象是其状态和行为的封装。

    类是按一定体系和层次安排的,使得子类可以从超类继承行为。在这个类层次体系中有一个根类,它是具有一般行为的类。Java程序是用类来组织的。

    对于java的泛型我一直属于一知半解的,平常真心用的不多。直到阅读《Effect Java》,看到很多平常不了解的用法,才下定决心,需要系统的学习,并且记录下来。

    带你了解编程语言Java的 泛型全解_编程语言_Java_JavaScript_课课家

    1、泛型的概述:

    1.1 泛型的由来

    根据《java编程思想》中的描述,泛型出现的动机:

    有很多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。

    泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:参数化类型

    1.2 基本概述

    • 泛型的本质就是"参数化类型"。一提到参数,最熟悉的就是定义方法的时候需要形参,调用方法的时候,需要传递实参。那"参数化类型"就是将原来具体的类型参数化
    • 泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行的错误。

    1.3 泛型的目的

    Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。

    1.4 实例

    JDk 1.5时增加了泛型,在很大的程度上方便在集合上的使用。

    不使用泛型:

    1. public static void main(String[] args) { 
    2.  List list = new ArrayList(); 
    3.  list.add(11); 
    4.  list.add("ssss"); 
    5.  for (int i = 0; i < list.size(); i++) { 
    6.  System.out.println((String)list.get(i)); 
    7.  } 
    8.  } 

    因为list类型是Object。所以int,String类型的数据都是可以放入的,也是都可以取出的。但是上述的代码,运行的时候就会抛出类型转化异常,这个相信大家都能明白。

    使用泛型:

    1. public static void main(String[] args) { 
    2.  List list = new ArrayList(); 
    3.  list.add("hahah"); 
    4.  list.add("ssss"); 
    5.  for (int i = 0; i < list.size(); i++) { 
    6.  System.out.println((String)list.get(i)); 
    7.  } 
    8.  } 

    在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。

    2、泛型的使用

    泛型的三种使用方式:泛型类,泛型方法,泛型接口

    2.1 泛型类

    泛型类概述:把泛型定义在类上

    定义格式:

    1. public class 类名 <泛型类型1,...> { 
    2.   

    注意事项:泛型类型必须是引用类型(非基本数据类型)

    2.2 泛型方法

    泛型方法概述:把泛型定义在方法上

    定义格式:

    public <泛型类型> 返回类型 方法名(泛型类型 变量名) { }

    注意要点:

    方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。

    1. class Demo{  
    2.  public  T fun(T t){ // 可以接收任意类型的数据  
    3.  return t ; // 直接把参数返回  
    4.  }  
    5. };  
    6. public class GenericsDemo26{  
    7.  public static void main(String args[]){  
    8.  Demo d = new Demo() ; // 实例化Demo对象  
    9.  String str = d.fun("汤姆") ; // 传递字符串  
    10.  int i = d.fun(30) ; // 传递数字,自动装箱  
    11.  System.out.println(str) ; // 输出内容  
    12.  System.out.println(i) ; // 输出内容  
    13.  }  
    14. }; 

    2.3 泛型接口

    泛型接口概述:把泛型定义在接口

    定义格式:

    1. public interface 接口名<泛型类型> { 
    2.   

    实例:

    1. /** 
    2.  * 泛型接口的定义格式: 修饰符 interface 接口名<数据类型> {} 
    3.  */ 
    4. public interface Inter { 
    5.  public abstract void show(T t) ; 
    6. /** 
    7.  * 子类是泛型类 
    8.  */ 
    9. public class InterImpl implements Inter { 
    10.  @Override 
    11.  public void show(E t) { 
    12.  System.out.println(t); 
    13.  } 
    14. Inter inter = new InterImpl() ; 
    15. inter.show("hello") ; 

    2.4 源码中泛型的使用,下面是List接口和ArrayList类的代码片段。

    1. //定义接口时指定了一个类型形参,该形参名为E 
    2. public interface List extends Collection { 
    3.  //在该接口里,E可以作为类型使用 
    4.  public E get(int index) {} 
    5.  public void add(E e) {}  
    6. //定义类时指定了一个类型形参,该形参名为E 
    7. public class ArrayList extends AbstractList implements List { 
    8.  //在该类里,E可以作为类型使用 
    9.  public void set(E e) { 
    10.  ....................... 
    11.  } 

    2.5 泛型类派生子类

    父类派生子类的时候不能在包含类型形参,需要传入具体的类型

    错误的方式:

    1. public class A extends Container {} 

    正确的方式:

    1. public class A extends Container {} 

    也可以不指定具体的类型,系统就会把K,V形参当成Object类型处理

    1. public class A extends Container {} 

    2.6 泛型构造器

    构造器也是一种方法,所以也就产生了所谓的泛型构造器。

    和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断

    1. public class Person { 
    2.  public  Person(T t) { 
    3.  System.out.println(t); 
    4.  } 
    5.   

    使用:

    1. public static void main(String[] args) { 
    2.  new Person(22);// 隐式 
    3.  new  Person("hello");//显示 

    特殊说明:

    如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表

    1. public class Person { 
    2.  public  Person(T t) { 
    3.  System.out.println(t); 
    4.  } 

    正确用法:

    1. public static void main(String[] args) { 
    2.  Person person = new Person("sss"); 

    PS:编译器会提醒你怎么做的

    2.7 高级通配符

    2.7.1背景:

    2.7.2 上界通配符

    上界通配符顾名思义,表示的是类型的上界【包含自身】,因此通配的参数化类型可能是T或T的子类。

    正因为无法确定具体的类型是什么,add方法受限(可以添加null,因为null表示任何类型),但可以从列表中获取元素后赋值给父类型。如上图中的第一个例子,第三个add()操作会受限,原因在于List和List是List的子类型。

    它表示集合中的所有元素都是Animal类型或者其子类 List

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

    例如:

    这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。然后进行其他操作。

    1. 它表示集合中的所有元素都是Animal类型或者其子类 
    2.  List 

    2.7.3 下界通配符

    下界通配符表示的是参数化类型是T的超类型(包含自身),层层至上,直至Object

    编译器无从判断get()返回的对象的类型是什么,因此get()方法受限。但是可以进行add()方法,add()方法可以添加T类型和T类型的子类型,如第二个例子中首先添加了一个Cat类型对象,然后添加了两个Cat子类类型的对象,这种方法是可行的,但是如果添加一个Animal类型的对象,显然将继承的关系弄反了,是不可行的。

    它表示集合中的所有元素都是Cat类型或者其父类 List

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

    例如

    1. //Animal是其父类 
    2. List list = new ArrayList(); 

    2.7.4 无界通配符

    任意类型,如果没有明确,那么就是Object以及任意的Java类了

    无界通配符用表示,?代表了任何的一种类型,能代表任何一种类型的只有null(Object本身也算是一种类型,但却不能代表任何一种类型,所以List和List的含义是不同的,前者类型是Object,也就是继承树的最上层,而后者的类型完全是未知的)

    3、泛型擦除

    3.1 概念

    编译器编译带类型说明的集合时会去掉类型信息

    3.2 验证实例:

    1. public class GenericTest { 
    2.  public static void main(String[] args) { 
    3.  new GenericTest().testType(); 
    4.  } 
    5.  public void testType(){ 
    6.  ArrayList<Integer> collection1 = new ArrayList<Integer>(); 
    7.  ArrayList collection2= new ArrayList(); 
    8.   
    9.  System.out.println(collection1.getClass()==collection2.getClass()); 
    10.  //两者class类型一样,即字节码一致 
    11.   
    12.  System.out.println(collection2.getClass().getName()); 
    13.  //class均为java.util.ArrayList,并无实际类型参数信息 
    14.  } 

    输出结果:

    1. true 
    2. java.util.ArrayList 

    分析:

    这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

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

    4、泛型与反射

    把泛型变量当成方法的参数,利用Method类的getGenericParameterTypes方法来获取泛型的实际类型参数

    例子:

    1. public class GenericTest { 
    2.  public static void main(String[] args) throws Exception { 
    3.  getParamType(); 
    4.  } 
    5.   
    6.  /*利用反射获取方法参数的实际参数类型*/ 
    7.  public static void getParamType() throws NoSuchMethodException{ 
    8.  Method method = GenericTest.class.getMethod("applyMap",Map.class); 
    9.  //获取方法的泛型参数的类型 
    10.  Type[] types = method.getGenericParameterTypes(); 
    11.  System.out.println(types[0]); 
    12.  //参数化的类型 
    13.  ParameterizedType pType = (ParameterizedType)types[0]; 
    14.  //原始类型 
    15.  System.out.println(pType.getRawType()); 
    16.  //实际类型参数 
    17.  System.out.println(pType.getActualTypeArguments()[0]); 
    18.  System.out.println(pType.getActualTypeArguments()[1]); 
    19.  } 
    20.  /*供测试参数类型的方法*/ 
    21.  public static void applyMap(Map<Integer,String> map){ 
    22.  } 

    输出结果:

    1. java.util.MapInteger, java.lang.String> 
    2. interface java.util.Map 
    3. class java.lang.Integer 
    4. class java.lang.String 

    通过反射绕开编译器对泛型的类型限制

    1. public static void main(String[] args) throws Exception { 
    2.         //定义一个包含int链表 
    3.         ArrayList<Integer> al = new ArrayList<Integer>(); 
    4.         al.add(1); 
    5.         al.add(2); 
    6.         //获取链表的add方法,注意这里是Object.class,如果写int.class会抛出NoSuchMethodException异常 
    7.         Method m = al.getClass().getMethod("add", Object.class); 
    8.         //调用反射中的add方法加入一个string类型的元素,因为add方法的实际参数是Object 
    9.         m.invoke(al, "hello"); 
    10.         System.out.println(al.get(2)); 
    11.     } 

    5 泛型的限制

    5.1 模糊性错误

    对于泛型类User

    1. public class User { 
    2.   
    3.  public void show(K k) { // 报错信息:'show(K)' clashes with 'show(V)'; both methods have same erasure 
    4.   
    5.  } 
    6.  public void show(V t) { 
    7.  } 

    由于泛型擦除,二者本质上都是Obejct类型。方法是一样的,所以编译器会报错。

    换一个方式:

    1. public class User { 
    2.  public void show(String k) { 
    3.  } 
    4.  public void show(V t) { 
    5.  } 

    使用结果:

    可以正常的使用5.2 不能实例化类型参数

    编译器也不知道该创建那种类型的对象

    1. public class User { 
    2.  private K key = new K(); // 报错:Type parameter 'K' cannot be instantiated directly 

    5.3 对静态成员的限制

    静态方法无法访问类上定义的泛型;如果静态方法操作的类型不确定,必须要将泛型定义在方法上。

    如果静态方法要使用泛型的话,必须将静态方法定义成泛型方法。

    1. public class User { 
    2.  //错误 
    3.  private static T t; 
    4.  //错误 
    5.  public static T getT() { 
    6.  return t; 
    7.  } 
    8.  //正确 
    9.  public static  void test(K k) { 
    10.  } 

    5.4 对泛型数组的限制

    不能实例化元素类型为类型参数的数组,但是可以将数组指向类型兼容的数组的引用

    1. public class User { 
    2.  private T[] values
    3.  public User(T[] values) { 
    4.  //错误,不能实例化元素类型为类型参数的数组 
    5.  this.values = new T[5]; 
    6.  //正确,可以将values 指向类型兼容的数组的引用 
    7.  this.values = values
    8.  } 

    5.5 对泛型异常的限制

    泛型类不能扩展 Throwable,意味着不能创建泛型异常类

     Java是一个强类型语言,它允许扩展编译时检查潜在类型不匹配问题的功能。Java要求显式的方法声明,它不支持C风格的隐式声明。这些严格的要求保证编译程序能捕捉调用错误,这就导致更可靠的程序。

课课家教育

未登录