首页 > 编程语言 >java 泛型 深入

java 泛型 深入

时间:2023-06-13 14:01:46浏览次数:53  
标签:java String class 类型 Base 深入 泛型 Class


评:

泛型的好处:

(casting)的绝对无误。 
 
         
  /** 
  ***** 不使用泛型类型 ****** 
  */ 
  
        List list1  
  = 
    
  new 
   ArrayList();
        list1.add( 
  8080 
  );                                   
  // 
  编译器不检查值 
  
 
          String str1  
  = 
   (String)list1.get( 
  0 
  );  
  // 
  需手动强制转换,如转换类型与原数据类型不一致将抛出ClassCastException异常 
  
 
          
         
  /** 
  ***** 使用泛型类型 ****** 
  */ 
  
        List 
  < 
  String 
  > 
   list2  
  = 
    
  new 
   ArrayList 
  < 
  String 
  > 
  ();
        list2.add( 
  " 
  value 
  " 
  );                  
  // 
  [类型安全的写入数据] 编译器检查该值,该值必须是String类型才能通过编译 
  
 
          String str2  
  = 
   list2.get( 
  0 
  );  
  // 
  [类型安全的读取数据] 不需要手动转换


泛型的类型擦除:

    Java 中的泛型只存在于编译期,在将 Java 源文件编译完成 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。

    这个过程就称为类型擦除(type erasure)。

List 
  < 
  String 
  > 
      list1  
  = 
    
  new 
   ArrayList 
  < 
  String 
  > 
  ();
        List 
  < 
  Integer 
  > 
   list2  
  = 
    
  new 
   ArrayList 
  < 
  Integer 
  > 
  ();
        
        System.out.println(list1.getClass()  
  == 
   list2.getClass());  
  // 
   输出结果: true 
  
 
          System.out.println(list1.getClass().getName());  
  // 
   输出结果: java.util.ArrayList 
  
 
          System.out.println(list2.getClass().getName());  
  // 
   输出结果: java.util.ArrayList


在以上代码中定义的 List<String> 和 List<Integer> 等类型,在编译之后都会变成 List,而由泛型附加的类型信息对 JVM 来说是不可见的,所以第一条打印语句输出 true,

第二、第三条打印语句都输出 java.util.ArrayList,这都说明 List<String> 和 List<Integer> 的对象使用的都是同一份字节码,运行期间并不存在泛型。

来看一个简单的例子:


package 
   test;

 
  import 
   java.util.List;
 
  /** 
  
 * -----------------------------------------
 * @描述  类型擦除
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
  
  */ 
  
 
  public 
    
  class 
   GenericsApp {

    
     
  public 
    
  void 
   method(List 
  < 
  String 
  > 
   list){
        
    }
    
     
  /* 
  
     * 编译出错,这两个方法不属于重载,由于类型的擦除,使得这两个方法的参数列表的参数均为List类型,
     * 这就相当于同一个方法被声明了两次,编译自然无法通过了
     * 
    public void method(List<Integer> list){
        
    }
     
  */ 
  
    
}


以此类为例,在 cmd 中 编译 GenericsApp.java 得到字节码,然后再反编译这份字节码:


java 泛型 深入_java



从图中可以看出,经反编译后的源码中 method 方法的参数变成了 List 类型,说明泛型的类型被擦除了,字节码文件中不存在泛型,也就是说,运行期间泛型并不存在,它在


编译完成之后就已经被擦除了。



泛型类型的子类型:


    泛型类型跟其是否是泛型类型的子类型没有任何关系。


List   < 
  Object 
  > 
   list1;
        List   < 
  String 
  > 
   list2;
        
        list1    = 
   list2;  
  // 
   编译出错 
  
           list2  
  = 
   list1;  
  // 
   编译出错


在 Java 中,Object 类是所有类的超类,自然而然的 Object 类是 String 类的超类,按理,将一个 String 类型的对象赋值给一个 Object 类型的对象是可行的,

但是泛型中并不存在这样的逻辑,泛型类型跟其是否子类型没有任何关系。


泛型中的通配符(?):

    由于泛型类型与其子类型存在不相关性,那么在不能确定泛型类型的时候,可以使用通配符(?),通配符(?)能匹配任意类型。


   

List   <?> 
   list;
        List   < 
  Object 
  > 
   list1  
  = 
    
  null 
  ;
        List   < 
  String 
  >  
   list2  
  = 
    
  null 
  ;
        
        list    = 
   list1;
        list    = 
   list2;

限定通配符的上界:

ArrayList   <? 
    
  extends 
   Number 
  > 
   collection  
  = 
    
  null 
  ;
        
        collection    = 
    
  new 
   ArrayList 
  < 
  Number 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Short 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Integer 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Long 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Float 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Double 
  > 
  ();

 ? extends XX,XX 类是用来限定通配符的上界,XX 类是能匹配的最顶层的类,它只能匹配 XX 类以及 XX 类的子类。在以上代码中,Number 类的实现类有:

AtomicInteger、AtomicLong、 BigDecimal、 BigInteger、 Byte、 Double、 Float、 Integer、 Long、 Short ,因此以上代码均无错误。


限定通配符的下界:

ArrayList   <? 
    
  super 
   Integer 
  > 
   collection  
  = 
    
  null 
  ;
        
        collection    = 
    
  new 
   ArrayList 
  < 
  Object 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Number 
  > 
  ();
        collection    = 
    
  new 
   ArrayList 
  < 
  Integer 
  > 
  ();


        


 ? super XX,XX 类是用来限定通配符的下界,XX 类是能匹配的最底层的类,它只能匹配 XX 类以及 XX 类的超类,在以上代码中,Integer 类的超类有:

Number、Object,因此以上代码均能通过编译无误。


通过反射获得泛型的实际类型参数:

    java.lang.Class 类从 Java 1.5 起(如果没记错的话),提供了一个 getGenericSuperclass() 方法来获取直接超类的泛型类型

package 
   test;

   import 
   java.lang.reflect.ParameterizedType;
   /** 
  
 * -----------------------------------------
 * @描述  泛型的实际类型参数
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Base 
  < 
  T 
  > 
   {

       private 
   Class 
  < 
  T 
  > 
   entityClass;
    
       // 
  代码块,也可将其放置到构造子中 
  
       {
        entityClass    = 
  (Class 
  < 
  T 
  > 
  )((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[ 
  0 
  ];
            
    }
    
       // 
  泛型的实际类型参数的类全名 
  
        
  public 
   String getEntityName(){
        
           return 
   entityClass.getName();
    }
    
       // 
  泛型的实际类型参数的类名 
  
        
  public 
   String getEntitySimpleName(){
        
           return 
   entityClass.getSimpleName();
    }

       // 
  泛型的实际类型参数的Class 
  
        
  public 
   Class 
  < 
  T 
  > 
   getEntityClass() {
           return 
   entityClass;
    }
    
}  
(Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];   相当于:
  
       // 
  代码块,也可将其放置到构造子中 
  
       {
           // 
  entityClass =(Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 
  
            
  try 
   {
            Class   <?> 
   clazz  
  = 
   getClass();  
  // 
  获取实际运行的类的 Class 
  
               Type type  
  = 
   clazz.getGenericSuperclass();  
  // 
  获取实际运行的类的直接超类的泛型类型 
  
                
  if 
  (type  
  instanceof 
   ParameterizedType){  
  // 
  如果该泛型类型是参数化类型 
  
                   Type[] parameterizedType  
  = 
   ((ParameterizedType)type).getActualTypeArguments(); 
  // 
  获取泛型类型的实际类型参数集 
  
                   entityClass  
  = 
   (Class 
  < 
  T 
  > 
  ) parameterizedType[ 
  0 
  ];  
  // 
  取出第一个(下标为0)参数的值 
  
               }
        }    catch 
   (Exception e) {
            e.printStackTrace();
        }
            
    }


注意,获取 Class 实例的时候是用 getClass(),而不是用 Base.class,获取 Class 的方式有三种,这是其中的两种,还有一种是 Class.forName("类全名"),如需了解反射的基础知识

请前往上一篇随笔 java 反射基础

那么,Base.class 与 getClass(),这两个方法来获取类的字节码的时候,Base.class 是写死了的,它得到的永远是 Base 类的字节码,

而 getClass() 方法则不同,在上面代码注释中的第一、二行注释我用了“实际运行的类”6个字,这几个字很重要,是一定要理解的。

为了方便大家的理解,下面插加一个小例子来加以说明 类.class 与 getClass() 两种方法来获取类的字节码有什么区别:


package 
   test;
   /** 
  
 * -----------------------------------------
 * @描述  超类
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Father {

       public 
   Father (){
        
        System.out.println(   " 
  Father 类的构造子: 
  " 
  );
        System.out.println(   " 
  Father.class : 
  " 
    
  + 
   Father. 
  class 
  );
        System.out.println(   " 
  getClass()      : 
  " 
    
  + 
   getClass());
    }
    
}  
 
  
   package 
   test;

   /** 
  
 * -----------------------------------------
 * @描述  超类的子类(超类的实现类)
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Children  
  extends 
   Father{

    
}  
 
  
   package 
   test;
   /** 
  
 * -----------------------------------------
 * @描述  测试类
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Test {

       public 
    
  static 
    
  void 
   main(String[] args){
        
           new 
   Children();  
  // 
  实际运行的类是Children(Father类的子类或者说是实现类) 
  
       }
    
}

后台打印输出的结果:


Father 类的构造子:
Father.   class 
   : 
  class 
   test.Father
getClass()      :   class 
   test.Children


 从打印出的结果看来,类.class 与 getClass() 的区别很明了了,getClass() 获取的是实际运行的类的字节码,它不一定是当前类的 Class,有可能是当前类的子类的 Class,具体是哪

个类的 Class,需要根据实际运行的类来确定,new 哪个类,getClass() 获取的就是哪个类的 Class,而 类.class 获取得到的 Class 永远只能是该类的 Class,这点是有很大的区别的。

如果 getClass() 理解了,那 clazz.getGenericSuperclass() 也应该能够理解了的,千万不要以为 clazz.getGenericSuperclass() 获取得到的是 Object 类那就成了,

实际上假如当前运行的类是 Base 类的子类,那么 clazz.getGenericSuperclass() 获取得到的就是 Base 类。

(Class<T>) parameterizedType[0],怎么就知道第一个参数(parameterizedType[0])就是该泛型的实际类型呢?很简单,因为 Base<T> 的泛型的类型

参数列表中只有一个参数,所以,第一个元素就是泛型 T 的实际参数类型。


package 
   test;
   /** 
  
 * -----------------------------------------
 * @描述  测试类
 * @作者  fancy

 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Test {

       public 
    
  static 
    
  void 
   main(String[] args){
        
        Base   < 
  String 
  > 
   base  
  = 
    
  new 
   Base 
  < 
  String 
  > 
  ();
        System.out.println(base.getEntityClass());                           // 
  打印输出 null
       // 
      System.out.println(base.getEntityName());                 
  // 
  抛出 NullPointerException 异常
       // 
      System.out.println(base.getEntitySimpleName());  
  // 
  抛出 NullPointerException 异常 
  
       }
    
}

从打印的结果来看,Base 类并不能直接来使用,为什么会这样?原因很简单,由于 Base 类中的 clazz.getGenericSuperclass() 方法,如果随随便便的就确定 Base 类的泛型的类型

参数,则很可能无法通过 Base 类中的 if 判断,导致 entityClass 的值为 null,像这里的 Base<String>,String 的 超类是 Object,而 Object 并不能通过 if 的判断语句。

Base 类不能够直接来使用,而是应该通过其子类来使用,Base 应该用来作为一个基类,我们要用的是它的具体的子类,下面来看下代码,它的子类也不是随便写的:


package 
   test;
   /** 
  
 * -----------------------------------------
 * @描述  Base类的实现类
 * @作者  fancy
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Child  
  extends 
   Base 
  < 
  Child 
  > 
  {

}

从上面代码来看,Base 的泛型类型参数就是 Base 的子类本身,这样一来,当使用 Base 类的子类 Child 类时,Base 类就能准确的获取到当前实际运行的类的 Class,来看下怎么使用

package 
   test;
   /** 
  
 * -----------------------------------------
 * @描述  测试类
 * @日期  2012-8-25 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Test {

       public 
    
  static 
    
  void 
   main(String[] args){
        
        Child child    = 
    
  new 
   Child();
        System.out.println(child.getEntityClass());
        System.out.println(child.getEntityName());
        System.out.println(child.getEntitySimpleName());
    }
    
}

后台打印输出的结果:


class  test.Child
test.Child
Child


好了,文章接近尾声了,如果你能理解透这个例子,你可以将这个思想运用到 DAO 层面上来,以 Base 类作为所有 DAO 实现类的基类,在 Base 类里面实现数据库的 CURD 等基本

操作,然后再使所有具体的 DAO 类来实现这个基类,那么,实现这个基类的所有的具体的 DAO 都不必再实现数据库的 CURD 等基本操作了,这无疑是一个很棒的做法。


(通过反射获得泛型的实际类型参数)补充:

泛型反射的关键是获取 ParameterizedType 接口,再调用 ParameterizedType 接口中的 getActualTypeArguments() 方法就可获得实际绑定的类型。

由于去参数化(擦拭法),也只有在 超类(调用 getGenericSuperclass 方法) 或者成员变量(调用 getGenericType 方法)或者方法(调用 getGenericParameterTypes 方法)

像这些有方法返回 ParameterizedType 类型的时候才能反射成功。

上面只谈到超类如何反射,下面将变量和方法的两种反射补上:

通过方法,反射获得泛型的实际类型参数:

package 
   test;

   import 
   java.lang.reflect.Method;
   import 
   java.lang.reflect.ParameterizedType;
   import 
   java.lang.reflect.Type;
   import 
   java.util.Collection;

   /** 
  
 * -----------------------------------------
 * @描述  测试类
 * @作者  fancy
 * @日期  2012-8-26 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Test {

       public 
    
  static 
    
  void 
   main(String[] args){
           /** 
  
         * 泛型编译后会去参数化(擦拭法),因此无法直接用反射获取泛型的参数类型
         * 可以把泛型用做一个方法的参数类型,方法可以保留参数的相关信息,这样就可以用反射先获取方法的信息
         * 然后再进一步获取泛型参数的相关信息,这样就得到了泛型的实际参数类型
            */ 
  
           try 
   {
            Class   <?> 
   clazz  
  = 
   Test. 
  class 
  ;  
  // 
  取得 Class 
  
               Method method  
  = 
   clazz.getDeclaredMethod( 
  " 
  applyCollection 
  " 
  , Collection. 
  class 
  );  
  // 
  取得方法 
  
               Type[] type  
  = 
   method.getGenericParameterTypes();  
  // 
  取得泛型类型参数集 
  
               ParameterizedType ptype  
  = 
   (ParameterizedType)type[ 
  0 
  ]; 
  // 
  将其转成参数化类型,因为在方法中泛型是参数,且Number是第一个类型参数 
  
               type  
  = 
   ptype.getActualTypeArguments();  
  // 
  取得参数的实际类型 
  
               System.out.println(type[ 
  0 
  ]);  
  // 
  取出第一个元素 
  
           }  
  catch 
   (Exception e) {
            e.printStackTrace();
        }
    }
    
       // 
  声明一个空的方法,并将泛型用做为方法的参数类型 
  
        
  public 
    
  void 
   applyCollection(Collection 
  < 
  Number 
  > 
   collection){
        
    }
}


后台打印输出的结果:


class  java.lang.Number


通过字段变量,反射获得泛型的实际类型参数:

package 
   test;

   import 
   java.lang.reflect.Field;
   import 
   java.lang.reflect.Method;
   import 
   java.lang.reflect.ParameterizedType;
   import 
   java.lang.reflect.Type;
   import 
   java.util.Collection;
   import 
   java.util.Map;

   /** 
  
 * -----------------------------------------
 * @描述  测试类
 * @作者  fancy
 * @日期  2012-8-26 <p>
 * -----------------------------------------
    */ 
  
   public 
    
  class 
   Test {

       private 
   Map 
  < 
  String, Number 
  > 
   collection;
    
       public 
    
  static 
    
  void 
   main(String[] args){
        
           try 
   {
            
            Class   <?> 
   clazz  
  = 
   Test. 
  class 
  ;  
  // 
  取得 Class 
  
               Field field  
  = 
   clazz.getDeclaredField( 
  " 
  collection 
  " 
  );  
  // 
  取得字段变量 
  
               Type type  
  = 
   field.getGenericType();  
  // 
  取得泛型的类型 
  
               ParameterizedType ptype  
  = 
   (ParameterizedType)type;  
  // 
  转成参数化类型 
  
               System.out.println(ptype.getActualTypeArguments()[ 
  0 
  ]);  
  // 
  取出第一个参数的实际类型 
  
               System.out.println(ptype.getActualTypeArguments()[ 
  1 
  ]);  
  // 
  取出第二个参数的实际类型 
  
               
        }    catch 
   (Exception e) {
            e.printStackTrace();
        }
    }
    
}

后台打印输出的结果:


class 
   java.lang.String
   class 
   java.lang.Number

标签:java,String,class,类型,Base,深入,泛型,Class
From: https://blog.51cto.com/u_16080829/6469288

相关文章

  • 【技术积累】JavaSciprt中的函数【一】
    什么是函数?如何声明函数?JavaScript中的函数是一段可重复使用的代码块,它可以接受输入并返回输出。在JavaScript中,函数是一种特殊的对象,因此可以将其存储在变量中,将其作为参数传递给其他函数,并从其他函数中返回。在JavaScript中,声明函数有两种方式:函数声明和函数表达式。1.函数......
  • Java8 Stream List Map:Stream 流对象汇总 求和 某个属性 BigDecimal MDouble
    测试实体(数字对象使用MDouble):importcom.mchweb.common.lang.MDouble;importlombok.*;@Getter@Setter@Builder(toBuilder=true)@NoArgsConstructor@AllArgsConstructorpublicclassUser{privateMDoublemoney;}importcom.mchweb.common.lang.MDouble;imp......
  • 你真的读懂了Java源码?Collections源码初探
    最近重温Java知识,遇到不懂的问题搜索互联网/博客很难直接找到答案,还好如今有了chatGPT,比大多数CV复读机/纯文档翻译的内容更有用。很多文章总结冠以“深入理解xxx”,“万字长文详解xxx”的文章,也不过是演示一遍调用代码,让你知道了怎么用,在什么情况下用。但至于为什么这么用,以及Java......
  • java WebUploader 分片上传
    ​ 对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。 本文是基于springboot+vue实现的文件上传,本文主要介绍服务端实现文件......
  • C# Type传参转换成泛型T
    publicclassTest{publicvoidExport<T1,T2>(){}publicvoidExportByClassName(stringtypename1,stringtypename2){Typet1=Type.GetType(typename1);Typet2=Type.GetType(typename2);Method......
  • Java 利用POI对象 SXSSFWorkbook 导出Excel
    最开始调用的方法是(标记的地方): workbook=newHSSFWorkbook();和workbook=newXSSFWorkbook();这两个方法就是导出Excel的最关键的方法,接下来我来说说这两个方法作用:1.HSSFWorkbook:是操作Excel2003以前(包括2003)的版本,扩展名是.xls;2.XSSFWorkbook:是操作Excel2007的版本......
  • Java判断一个List中是否有重复元素
    1.将List转为Set,通过2个集合的size大小是否相等来判断有无重复元素publicstaticvoidmain(String[]args){ListstringList=newArrayList<>(Arrays.asList("a","a","b","c"));SetstringSet=newHashSet<>(stringList);......
  • Javascript中的内存(垃圾)回收机制
    JavaScript具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行常见的垃圾回收方式:标记清除、引用计数方式。一、标记清除方法1、工作原理:【标记“离开环境”的就回收内存】当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“......
  • springcloud 启动失败 YAMLException java.nio.charset.MalformedInputException Inp
     上面这个是错误信息,但是该微服务在本地启动的时候是可以的,但是本地打成jar包本地执行的时候就失败。需要再Java-jar的中间加一下字符编码java-Dfile.encoding=utf-8-jar  myself.jar   myself.jar是自己的jar包问题解决......
  • java 获取ftp文件列表以及模糊查询,并对结果进行分页
    /***获取ftp文件列表*".*\\.txt":匹配所有以".txt"结尾的文件名。其中,星号(*)表示任意字符序列,反斜杠(\)用于转义点号(.)字符。*".*"+"任意字符"+".*\\.txt":匹配所有包含"表示匹配任意多个任意字符"和以".txt"结尾的文件名。其中,星号(*)表示任意字......