• Java 泛型


    Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

    泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

    假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

    答案是可以使用 Java 泛型

    使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。


    泛型方法

    你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

    下面是定义泛型方法的规则:

    • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。
    • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
    • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
    • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

    实例

    下面的例子演示了如何使用泛型方法打印不同字符串的元素:

    实例

    public class GenericMethodTest { // 泛型方法 printArray public static < E > void printArray( E[] inputArray ) { // 输出数组元素 for ( E element : inputArray ){ System.out.printf( "%s ", element ); } System.out.println(); } public static void main( String args[] ) { // 创建不同类型数组: Integer, Double 和 Character Integer[] intArray = { 1, 2, 3, 4, 5 }; Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 }; Character[] charArray = { 'H', 'E', 'L', 'L', 'O' }; System.out.println( "整型数组元素为:" ); printArray( intArray ); // 传递一个整型数组 System.out.println( " 双精度型数组元素为:" ); printArray( doubleArray ); // 传递一个双精度型数组 System.out.println( " 字符型数组元素为:" ); printArray( charArray ); // 传递一个字符型数组 } }

    编译以上代码,运行结果如下所示:

    整型数组元素为:
    1 2 3 4 5 
    
    双精度型数组元素为:
    1.1 2.2 3.3 4.4 
    
    字符型数组元素为:
    H E L L O 

    有界的类型参数:

    可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

    要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

    实例

    下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口)。该例子中的泛型方法返回三个可比较对象的最大值。

    实例

    public class MaximumTest { // 比较三个值并返回最大值 public static <T extends Comparable<T>> T maximum(T x, T y, T z) { T max = x; // 假设x是初始最大值 if ( y.compareTo( max ) > 0 ){ max = y; //y 更大 } if ( z.compareTo( max ) > 0 ){ max = z; // 现在 z 更大 } return max; // 返回最大对象 } public static void main( String args[] ) { System.out.printf( "%d, %d 和 %d 中最大的数为 %d ", 3, 4, 5, maximum( 3, 4, 5 ) ); System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f ", 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) ); System.out.printf( "%s, %s 和 %s 中最大的数为 %s ","pear", "apple", "orange", maximum( "pear", "apple", "orange" ) ); } }

    编译以上代码,运行结果如下所示:

    3, 4  5 中最大的数为 5
    
    6.6, 8.8  7.7 中最大的数为 8.8
    
    pear, apple  orange 中最大的数为 pear

    泛型类

    泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

    和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

    实例

    如下实例演示了我们如何定义一个泛型类:

    实例

    public class Box<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box<Integer> integerBox = new Box<Integer>(); Box<String> stringBox = new Box<String>(); integerBox.add(new Integer(10)); stringBox.add(new String("菜鸟教程")); System.out.printf("整型值为 :%d ", integerBox.get()); System.out.printf("字符串为 :%s ", stringBox.get()); } }

    编译以上代码,运行结果如下所示:

    整型值为 :10
    
    字符串为 :菜鸟教程

    类型通配符

    1、类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。

    实例

    public class GenericTest { public static void main(String[] args) { List<String> name = new ArrayList<String>(); List<Integer> age = new ArrayList<Integer>(); List<Number> number = new ArrayList<Number>(); name.add("icon"); age.add(18); number.add(314); getData(name); getData(age); getData(number); } public static void getData(List<?> data) { System.out.println("data :" + data.get(0)); } }

    输出结果为:

    data :icon
    data :18
    data :314

    解析: 因为getData()方法的参数是List类型的,所以name,age,number都可以作为这个方法的实参,这就是通配符的作用

    2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

    实例

    public class GenericTest { public static void main(String[] args) { List<String> name = new ArrayList<String>(); List<Integer> age = new ArrayList<Integer>(); List<Number> number = new ArrayList<Number>(); name.add("icon"); age.add(18); number.add(314); //getUperNumber(name);//1 getUperNumber(age);//2 getUperNumber(number);//3 } public static void getData(List<?> data) { System.out.println("data :" + data.get(0)); } public static void getUperNumber(List<? extends Number> data) { System.out.println("data :" + data.get(0)); } }

    输出结果:

    data :18
    data :314

    解析: 在(//1)处会出现错误,因为getUperNumber()方法中的参数已经限定了参数泛型上限为Number,所以泛型为String是不在这个范围之内,所以会报错

    3、类型通配符下限通过形如 List<? super Number>来定义,表示类型只能接受Number及其三层父类类型,如Objec类型的实例。

    元组

    public class TwoTuple<A,B> {
        public final A first;
        public final B second;
        public TwoTuple(A a, B b){first = a; second = b;}
    }
    

    泛型接口

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

    泛型方法

    public <T> void function(T i){ //将泛型参数列表置于返回类型之前
    } 
    

    泛型类

    public class Generator<T>{
        private T mVar;
        public void set(T var){
          this.mVar = var;
        }
        public T get(){
          return mVar;
        }
    }
    

    擦除

    java中泛型擦除的原因

    假设某个应用程序具有两个类库 X/Y,并且 Y 还要使用类库 Z。随着 Java SE5的出现,这个应用程序和这些类库的创建者最终可能希望迁移到泛型上。但是,迁移是个大工程,不能为了迁移而迁移。所以,为了实现迁移兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。这样,它们不能拥有探测其他类库是否使用了泛型的能信。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。试想,如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型的开发者们说再见了。正因为类库对于编程语言极其重要,所以这不是一种可以接受的代价。擦除是否是最佳的或者唯一的迁移途径,还需要时间来检验。

    在泛型代码内部,无法获得任何有关泛型参数类型的信息。Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List< String >和List< Integer >在运行时事实上是相同的类型。

    List<Integer> intList = new ArrayList<>();
    List<String> stringList = new ArrayList<>();
    System.out.println(intList.getClass() == stringList.getClass()); //true
    

    既然泛型类的内部持有类型被抹掉,那么为什么我们在调用Generator#get()的时候还能够得到它所持有的具体类型呢?下面就看看是如何实现的。

    擦除的补偿

    Java泛型在instanceof、创建类型实例,创建数组、转型时都会有问题。有时必须通过引入类型标签(即你的类型的Class对象)进行补偿。使用动态的isInstance()方法,而不是instanceof。

    public class _15_GenericHolder<T> {
        private T obj;
    
        public void set(T obj) {
            this.obj = obj;
        }
    
        public T get() {
            return obj;
        }
    
        public static void main(String[] args) {
            _15_GenericHolder<String> holder = new _15_GenericHolder<String>();
            holder.set("Item");
            // 这里没有转型了,但是我们知道传递给 set()的值在编译期还是会接受检查
            String s = holder.get();
        }
    }
    
    // 反编译:
    public class Chapter15._15_GenericHolder<T> {
      public Chapter15._15_GenericHolder();
        Code:
           0: aload_0
           1: invokespecial #12                 // Method java/lang/Object."<init>":()V
           4: return
    
      public void set(T);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #23                 // Field obj:Ljava/lang/Object;
           5: return
    
      public T get();
        Code:
           0: aload_0
           1: getfield      #23                 // Field obj:Ljava/lang/Object;
           4: areturn
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #1                  // class Chapter15/_15_GenericHolder
           3: dup
           4: invokespecial #30                 // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: ldc           #31                 // String Item
          11: invokevirtual #33                 // Method set:(Ljava/lang/Object;)V
          14: aload_1
          15: invokevirtual #35                 // Method get:()Ljava/lang/Object;
          18: checkcast     #37                 // class java/lang/String
          21: astore_2
          22: return
    }
    

    上面的代码中可以在main的18行看见,有一次类型的强转,这是通过编译器实现的,也就是说当我们编译的时候代码其实就已经对它指定了返回类型,而不是在运行时判断返回类型。我们知道,实际的持有对象已经向上转型成为Object,也就是说我们的泛型只能使用Object的方法,而不能获取到真正的持有类的方法,于是,擦除边界就诞生了。

    public class Base{
      public void func(){
         System.out.println(This is Base)}
    }
    
    public class Holder<T extends Base>{
      T mVar;
      public void set(T var){
        this.mVar = var;
      }
      public func(){
        this.mVar.func();
      }
    }
    

    通过这样,我们的持有类型其实就由原本的Object变为了Base,这样我们就可以调用Base中的方法。同时还可以extends多个接口。

    interface Animal{
        public void speek();
    }
    interface Fish{
        public void bubble();
    }
    class GoldenFish implements Animal, Fish{
        @Override
        public void bubble() {
            System.out.println("O。.");
        }
        @Override
        public void speek() {
            System.out.println("wow~");
        }
    }
    class HoldItem<T>{
        T item;
        HoldItem(T item){ this.item = item; }
        T getItem() { return item; }
    }
    class Item1<T extends Animal & Fish> extends HoldItem<T>{
        Item1(T item){ super(item); }
        public void doSomething(){
            item.speek();
            item.bubble();
        }
    }
    

    这里的Item的持有对象必须是AnimalFish为了在泛型类中能够判断类型,可以引入类型标签.

    class ClassTypeCapture<T> {
        Class<T> kind;
        public ClassTypeCapture(Class<T> kind){
            this.kind = kind;
        }
        public boolean f(Object arg){
            return kind.isInstance(arg);
        }
        public static void main(String[] args){
            ClassTypeCapture<String> ctc = new ClassTypeCapture<String>(String.class);
            System.out.println(ctc.f("art")); // true
            System.out.println(ctc.f(1));  // false
        }
    }
    

    泛型数组

    public class ArrayItem<T>{};
    public class Demo{
      private ArrayItem<Integer> array;
      public Demo(int size){
        //this.array = (ArrayItem<Integer>[])new Object[size];//运行时错误:ClassCastException
        this.array = (ArrayItem<Integer>[])new ArrayItem[size];
      }
    }
    

    成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。

    参考

    在以前的版本中使用泛型类型,需要在声明并赋值的时候,两侧都加上泛型类型。例如:

    Map<String, String> myMap = new HashMap<String, String>();

    不过,在Java SE 7中,这种方式得以改进,现在你可以使用如下语句进行声明并赋值:

    Map<String, String> myMap = new HashMap<>();    //注意后面的"<>"

    在这条语句中,编译器会根据变量声明时的泛型类型自动推断出实例化HashMap时的泛型类型。再次提醒一定要注意new HashMap后面的「<>」,只有加上这个「<>」才表示是自动类型推断,否则就是非泛型类型的HashMap,并且在使用编译器编译源代码时会给出一个警告提示。

    注意:Java SE 7在创建泛型实例时的类型推断是有限制的,你只能在联系上下文可以明确确定参数化类型的时候使用泛型推断。例如:下面的例子无法正确编译:

    1. List<String> list = new ArrayList<>();
    2. list.add("A");
    3. // 由于addAll期望获得Collection<? extends String>类型的参数,因此下面的语句无法通过
    4. list.addAll(new ArrayList<>());

    与上面的例子相比,下面的这个例子可以通过编译:

    1. List<String> list = new ArrayList<>();
    2. list.add("A");
    3. List<? extends String> list2 = new ArrayList<>();
    4. list.addAll(list2);
  • 相关阅读:
    ==和equals
    java 多重继承
    java单例模式
    基础小知识
    print流之错误日志
    print流
    实现读文本文件(IOl流)
    缓冲流(数据的复制粘贴)IO流
    力扣20题、1047(括号合法性,删除字符串中的所有相邻重复项)
    力扣232题、225题(栈实现队列,队列实现栈)
  • 原文地址:https://www.cnblogs.com/shizhijie/p/8258738.html
Copyright © 2020-2023  润新知