zoukankan      html  css  js  c++  java
  • 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);
  • 相关阅读:
    Linnia学习记录
    漫漫考研路
    ENS的学习记录
    KnockoutJS 3.X API 第四章 数据绑定(4) 控制流with绑定
    KnockoutJS 3.X API 第四章 数据绑定(3) 控制流if绑定和ifnot绑定
    KnockoutJS 3.X API 第四章 数据绑定(2) 控制流foreach绑定
    KnockoutJS 3.X API 第四章 数据绑定(1) 文本及样式绑定
    KnockoutJS 3.X API 第三章 计算监控属性(5) 参考手册
    KnockoutJS 3.X API 第三章 计算监控属性(4)Pure computed observables
    KnockoutJS 3.X API 第三章 计算监控属性(3) KO如何实现依赖追踪
  • 原文地址:https://www.cnblogs.com/shizhijie/p/8258738.html
Copyright © 2011-2022 走看看