Java编程手册-泛型

Posted DroidMind

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java编程手册-泛型相关的知识,希望对你有一定的参考价值。

1. 泛型的引入(JDK 1.5)
在方法中传入一个参数,这个大家一定非常熟悉,一般的做法就是把参数放在一个圆括号()中并且将他们传递给方法,在泛型中,我们可以跟方法中传递参数一样来传递类型信息,做法就是将类型放在一个尖括号<>中。

JDK 1.5中引入了泛型,它允许我们对类型进行参数化,也就是类的设计者在类的定义过程中使用泛型,这样使用者在类的实例化或者方法调用的时候可以动态指定类型,这样在类的内部就可以动态使用这个类型了。

例如,类ArrayList就是被设计者设计成泛型,它带有一个泛型类型<E>。

public class ArrayList<E> implements List<E> .... {
   // Constructor
   public ArraList() { ...... }

   // Public methods
   public boolean add(E e) { ...... }
   public void add(int index, E element) { ...... }
   public boolean addAll(int index, Collection<? extends E> c)
   public abstract E get(int index) { ...... }
   public E remove(int index)
   .......
}

当在实例化一个ArrayList的时候,使用者就需要为E指定一个具体的类型,这个具体的类型就会取代类中所有的E,也就是类中使用到类型E的地方全部被取代为我们指定的真实类型。

ArrayList<Integer> lst1 = new ArrayList<Integer>(); // E substituted with Integer
lst1.add(0, new Integer(88));
lst1.get(0);
 
ArrayList<String> lst2 = new ArrayList<String>();   // E substituted with String
lst2.add(0, "Hello");
lst2.get(0);

上面的例子就是类的设计者在定义类的时候使用到了泛型,这样使用者在实例化这个类的时候,就需要为泛型类型E指定一个真实的类型,类型信息的传递是通过<>传递的,这就类似于方法参数通过()传递一样。

在集合引入泛型之前都是类型不安全的
如果大家对于JDK 1.5之前版本的集合比较属性的话,应该知道集合内部的元素类型都是使用的java.lang.Object,它使用的就是一种多态的原理,因为任何Object的子类都可以被Object代替,而Object是Java中所有对象类型的超类,这样我们集合中就可以存放任何的对象类型了。但是它存在一个明显的问题,假如我们定义了一个存放String对象的ArrayList,在add(Object)的时候,我们的String类型就会被向上转换为Object类型,这个是编译器隐式操作的,但是当我们获取这个元素的时候,我们获取的是Object类型的对象,这个时候,我们需要手动显式的将Object类型对象转换为String类型的对象,如果我们将得到的Object类型对象转换为一个非String类型的对象或者我们add(Object)存放的是一个非String对象,获取这个元素的时候将它转化为String类型,这个时候,编译器是检查不出来错误的,但是在运行的时候进行手动类型转换的时候就会抛出ClassCastException异常。

// Pre-JDK 1.5
import java.util.*;
public class ArrayListWithoutGenericsTest {
   public static void main(String[] args) {
      List strLst = new ArrayList();  // List and ArrayList holds Objects
      strLst.add("alpha");            // String upcast to Object implicitly
      strLst.add("beta");
      strLst.add("charlie");
      Iterator iter = strLst.iterator();
      while (iter.hasNext()) {
         String str = (String)iter.next(); // need to explicitly downcast Object back to String
         System.out.println(str);
      }
      strLst.add(new Integer(1234));       // Compiler/runtime cannot detect this error
      String str = (String)strLst.get(3);  // compile ok, but runtime ClassCastException
   }
}
为了解决这个问题,也就是在编译的时候就可以进行类型的检查,这样就引入了泛型。


2. 泛型

下面是一个自定义的ArrayList版本,叫做MyArrayList,它没有使用泛型。

// A dynamically allocated array which holds a collection of java.lang.Object - without generics
public class MyArrayList {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyArrayList() {         // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   public void add(Object o) {
      if (size < elements.length) {
         elements[size] = o;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   public Object get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return elements[index];
   }
   
   public int size() { return size; }
}
从上面很轻易的就可以看出MyArrayList不是类型安全的,例如,如果我们希望创建一个存放String类型对象的MyArrayList,但是我们向里面添加了一个Integer对象,编译器是不能检查出异常,这是因为我们MyArrayList设计的是存放Object类型的元素,并且任何对象类型都可以向上转换为Object类型的对象。

public class MyArrayListTest {
   public static void main(String[] args) {
      // Intends to hold a list of Strings, but not type-safe
      MyArrayList strLst = new MyArrayList();
      // adding String elements - implicitly upcast to Object
      strLst.add("alpha");
      strLst.add("beta");
      // retrieving - need to explicitly downcast back to String
      for (int i = 0; i < strLst.size(); ++i) {
         String str = (String)strLst.get(i);
         System.out.println(str);
      }
   
      // Inadvertently added a non-String object will cause a runtime
      // ClassCastException. Compiler unable to catch the error.
      strLst.add(new Integer(1234));   // compiler/runtime cannot detect this error
      for (int i = 0; i < strLst.size(); ++i) {
         String str = (String)strLst.get(i);   // compile ok, runtime ClassCastException
         System.out.println(str);
      }
   }
}

从上面可以看出,如果我们想创建一个String类型的List,但是我们添加了一个非String类型的对象元素,这个对象同样是可以向上转换为Object对象类型的,并且编译器自动完成,编译器并不能检查它是否合法,这样就存在一个隐患,当我们获取这个元素的时候,它是一个Object类型,我们需要手动转换为String类型,这个时候就会抛出ClassCastException异常,它发生在运行时期。


2.1 泛型类

JDK 1.5引入了所谓的泛型来解决这一问题,泛型允许我们去进行类型的抽象,我们可以创建一个泛型类并且在类实例化的时候指定具体类型信息。编译器在编译器期间会进行相应的类型检查,这样就确保了在运行时期不会有类型转换的异常发生,这就是所谓的类型安全。

下面我们来看看java.util.List<E>的声明接口。

public interface List<E> extends Collection<E> {
   boolean add(E o);
   void add(int index, E element);
   boolean addAll(Collection<? extends E> c);
   boolean containsAll(Collection<?> c);
   ......
}

<E>就是形式化的类型参数,在类实例化的时候就可以传递真实的类型参数进去来替换这个形式化的类型参数。

这个跟方法的调用是一样的,在定义方法的时候我们会声明形参,在调用方法的时候,形参就会接受实参进来。

例如:方法的定义,声明形参

// A method's definition
public static int max(int a, int b) {  // int a, int b are formal parameters
   return (a > b) ? a : b;
}
方法的调用,传递实参

// Invocation: formal parameters substituted by actual parameters
int maximum = max(55, 66);   // 55 and 66 are actual parameters
int a = 77, b = 88;
maximum = max(a, b);         // a and b are actual parameters
返回到上面的java.util.List<E>,假如我们创建的了一个List<Integer>,这个时候,形参类型E就接受到了一个实参类型<Integer>,这样我们在使用E的时候实际就是使用这个实参类型Integer了。

正式的类型参数命名规范

一般使用一个大写的字母作为类型参数。例如:

  • <E>表示一个集合元素的类型
  • <T>表示一个类型
  • <K, V>表示键和值的类型
  • <N>表示一个数字类型
  • S,U,V,等表示第二个、第三个、第四个类型参数
泛型类的例子
下面创建了一个GenericBox,它接收一个泛型类型E,表示一个content的类型,在构造器、getter、setter中都使用到了这个参数化类型E,toString方法返回的是它的真实类型。

public class GenericBox<E> {
   // Private variable
   private E content;
 
   // Constructor
   public GenericBox(E content) {
      this.content = content;
   }
 
   public E getContent() {
      return content;
   }
 
   public void setContent(E content) {
      this.content = content;
   }
 
   public String toString() {
      return content + " (" + content.getClass() + ")";
   }
}

下面使用不同的类型(String, Integer and Double)来检测这个GenericBoxes类,需要注意的是JDK 1.5也引入了基本数据类型与对应对象类型直接的自动装箱和解箱操作。

public class TestGenericBox {
   public static void main(String[] args) {
      GenericBox<String> box1 = new GenericBox<String>("Hello");
      String str = box1.getContent();  // no explicit downcasting needed
      System.out.println(box1);
      GenericBox<Integer> box2 = new GenericBox<Integer>(123);  // autobox int to Integer
      int i = box2.getContent();       // downcast to Integer, auto-unbox to int
      System.out.println(box2);
      GenericBox<Double> box3 = new GenericBox<Double>(55.66);  // autobox double to Double
      double d = box3.getContent();     // downcast to Double, auto-unbox to double
      System.out.println(box3);
   }
}

输出结果:

Hello (class java.lang.String)
123 (class java.lang.Integer)
55.66 (class java.lang.Double)

移除类型
从上面的例子我们可以看到,似乎编译器在类实例的时候使用真实类型(例如String, Integer)替换到了参数化类型E,事实上,编译器是使用Object类型替换到了类中所有的参数化类型E,只是编译器会针对传入的真实类型参数来进行类型检测和转换,这样做的目的是它可以跟之前非泛型的类进行兼容,并且同一个类可以使用所有对象类型参数。这个过程就叫做类型的移除。

下面我们返回到上面我们写的MyArrayList的例子,我们知道它不是一个泛型类型,下面我们来写一个泛型的版本。

// A dynamically allocated array with generics
public class MyGenericArrayList<E> {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyGenericArrayList() {  // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   public void add(E e) {
      if (size < elements.length) {
         elements[size] = e;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   public E get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return (E)elements[index];
   }
   
   public int size() { return size; }
}

解析程序
MyGenericArrayList<E>声明了一个带有类型参数E的泛型类,在真实实例化的时候,例如:MyGenericArrayList<String>, 真实类型<String>会取代参数类型E。编译器对于泛型的处理其实就是将泛型代码转换或者重写成非泛型代码,这样确保了向后兼容性,这个过程也就是类型移除,例如将ArrayList <Integer>转换为ArrayList,也就是类型参数E默认会被Object类型替代,当出现类型不匹配的时候,编译器就会插入类型转换操作。

MyGenericArrayList<E>被编译器处理后形式如下:

// The translated code
public class  MyGenericArrayList {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyGenericArrayList() {  // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   // Compiler replaces E with Object, but check e is of type E, when invoked to ensure type-safety
   public void add(Object e) {
      if (size < elements.length) {
         elements[size] = e;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   // Compiler replaces E with Object, and insert downcast operator (E<E>) for the return type when invoked
   public Object get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return (Object)elements[index];
   }
   
   public int size() { 
      return size; 
   }
}

当类使用真实类型参数进行实例化的时候,例如:MyGenericArrayList<String>,这样编译器就会确保add(E e)只能针对String类型进行操作,当使用get()返回类型E的对象的时候,编译器也会插入相应的类型转换操作来匹配类型String。

例如

public class MyGenericArrayListTest {
   public static void main(String[] args) {
      // type safe to hold a list of Strings
      MyGenericArrayList<String> strLst = new MyGenericArrayList<String>();
   
      strLst.add("alpha");   // compiler checks if argument is of type String
      strLst.add("beta");
   
      for (int i = 0; i < strLst.size(); ++i) {
         String str = strLst.get(i);   // compiler inserts the downcasting operator (String)
         System.out.println(str);
      }
   
      strLst.add(new Integer(1234));  // compiler detected argument is NOT String, issues compilation error
   }
}

可以看到,其实泛型跟非泛型的其实是相同的,只是对于泛型,编译器会针对传入的真实类型在编译期间进行类型检查,确保类型的一致性,这样就避免了运行时的类型安全问题。

跟C++中模板不同的是,在C++中,对于每一个指定的参数类型都会单独创建一个新的类,但是在Java中,泛型类编译一次之后就可以被每一个指定的类型参数进行使用。


2.2 泛型方法
方法也可以定义为泛型类型,例如:

public static <E> void ArrayToArrayList(E[] a, ArrayList<E> lst) {
   for (E e : a) lst.add(e);
}
在泛型方法中,需要在返回类型之前声明类型参数,这样类型参数就可以在方法的参数列表或者返回类型上使用了。

和泛型类相似,当编译器也会使用Object类型来替换参数类型E,例如上面的代码被编译器处理后形式如下:

public static void ArrayToArrayList(Object[] a, ArrayList lst) {  // compiler checks if a is of type E[],
                                                                  //   lst is of type ArrayList<E>
   for (Object e : a) lst.add(e);                                 // compiler checks if e is of type E
}

同样编译器添加了类型的检查的操作来确保类型安全。它会检查a是否为类型E[],lst是否为类型ArrayList<E>,e是否为类型E,其中参数类型E会根据传入真实类型动态确定。
import java.util.*;
public class TestGenericMethod {
   
   public static <E> void ArrayToArrayList(E[] a, ArrayList<E> lst) {
      for (E e : a) lst.add(e);
   }
   
   public static void main(String[] args) {
      ArrayList<Integer> lst = new ArrayList<Integer>();
   
      Integer[] intArray = {55, 66};  // autobox
      ArrayToArrayList(intArray, lst);
      for (Integer i : lst) System.out.println(i);
   
      String[] strArray = {"one", "two", "three"};
      //ArrayToArrayList(strArray, lst);   // Compilation Error below
   }
}

另外,在泛型方法中,泛型有一个可选的语法就是指定泛型方法中的类型。你可以将指定的真实类型放在点操作符和方法名之间。

TestGenericMethod.<Integer>ArrayToArrayList(intArray, lst);
这个语法可以增加代码的可读性,另外可以在类型模糊的地方来指定泛型类型。


2.3 通配符

对于下面这行代码

ArrayList<Object> lst = new ArrayList<String>();
它会出现类型不兼容的错误,因为ArrayList<String>不是一个ArrayList<Object>。这个问题貌似混淆了多态的概念,因为在多态中子类实例是可以分配给父类引用的。

对应下面代码:

List<String> strLst = new ArrayList<String>();   // 1
List<Object> objLst = strList;                   // 2 - Compilation Error非受限的通配符<?>
跟上面一样,第二行代码会出现编译错误,但是如果第二行代码成功的话,就会出现另一个问题:任意的对象都可以添加到strList中,这又会引起类型不安全的问题。

对应上面问题,我们可以看到,如果希望写一个方法printList(List<.>)来打印List的所有元素,如果我们定义方法为 printList(List<Object> lst),那么方法就只能接受List<object>类型的参数,不能接受参数List<String> 或者 List<Integer>,例如:
import java.util.*;
public class TestGenericWildcard {
   
   public static void printList(List<Object> lst) {  // accept List of Objects only, 
                                                     // not List of subclasses of object
      for (Object o : lst) System.out.println(o);
   }
   
   public static void main(String[] args) {
      List<Object> objLst = new ArrayList<Object>();
      objLst.add(new Integer(55));
      printList(objLst);   // matches
   
      List<String> strLst = new ArrayList<String>();
      strLst.add("one");
      printList(strLst);  // compilation error
   }
}


非受限的通配符<?>

为了解决这个问题,泛型中引入了一个通配符(?),它代表任何未知类型,例如我们可以重写上面的printList()方法,它可以接受任何未知类型的List。

public static void printList(List<?> lst) {
  for (Object o : lst) System.out.println(o);
}

上限通配符 <? extends type>

通配符<? extends type>表示接受类型type以及它的子类,例如:

public static void printList(List<? extends Number> lst) {
  for (Object o : lst) System.out.println(o);
}

List<? extends Number>接受Number以及Number子类型的List,例如:List<Integer> 和 List<Double>。

很显然,<?>可以理解为<? extends Object>,因为它可以接受任何对象类型。


下限通配符<? super type>

跟上限通配符类似,<? super type>表示接受的类型是type以及type的父类


2.4 受限泛型

在使用泛型的时候,我们也可以使用上面的限制来指定参数类型。例如:<T extends Number>表示接收Number以及它的子类(例如:Integer 和 Double)

例子
下面方法add()中声明了参数类型<T extends Number>

public class MyMath {
   public static <T extends Number> double add(T first, T second) {
      return first.doubleValue() + second.doubleValue();
   }
 
   public static void main(String[] args) {
      System.out.println(add(55, 66));     // int -> Integer
      System.out.println(add(5.5f, 6.6f)); // float -> Float
      System.out.println(add(5.5, 6.6));   // double -> Double
   }
}

编译器是如何对待受限泛型的呢?
上面我们说过,默认情况下,所有的泛型类型会被Object类型替换,但是对于受限类型会有些不同,例如<? extends Number>中的泛型类型会被Number类型替换。

例如:

public class TestGenericsMethod {
   public static <T extends Comparable<T>> T maximum(T x, T y) {
      return (x.compareTo(y) > 0) ? x : y;
   }
   
   public static void main(String[] args) {
      System.out.println(maximum(55, 66));
      System.out.println(maximum(6.6, 5.5));
      System.out.println(maximum("Monday", "Tuesday"));
   }
}

默认情况下,Object是所有参数类型的上限类型,但是在<T extends Comparable<T>>中,它显示的指定了上限类型为Comparable,因此编译器会将参数类型转换为Comparable类型。

public static Comparable maximum(Comparable x, Comparable y) {   // replace T by upper bound type Comparable
                                                                 // Compiler checks x, y are of the type Comparable
                                                                 // Compiler inserts a type-cast for the return value
   return (x.compareTo(y) > 0) ? x : y;
}

当方法被调用的时候,例如maximum(55, 66),基本数据类型int会被装箱为Integer对象,然后就会被隐式的转换为Comparable类型,编译器会进行类型的检查来确保类型安全,对于返回类型,它也会显式的插入类型转换操作。

(Comparable)maximum(55, 66);
(Comparable)maximum(6.6, 5.5);
(Comparable)maximum("Monday", "Tuesday");

我们不需要传递真实的类型参数给泛型方法,因为编译器会根据传入的参数自动的确定参数类型并进行类型的转换。


原文链接:Java Programming Tutorial Generics

以上是关于Java编程手册-泛型的主要内容,如果未能解决你的问题,请参考以下文章

java中的泛型

java泛型泛型的内部原理:类型擦除以及类型擦除带来的问题

Java笔记-泛型与集合框架

Java这个泛型不太正经

Java基础总结三(泛型异常)

Java基础总结三(泛型异常)