JAVA泛型详解
最近在工作中经常用到JAVA泛型相关的问题,最近看了几篇文章,总结一下。
什么是泛型?
泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
看着上面这段话有些拗口的话,我来解释一下:就在在List的<>里面可以加上String类型,比如以前的程序:
import java.util.Map; import java.util.HashMap; public class Account { public static void main(String[] args) throws InterruptedException { Map m = new HashMap(); m.put("key", "HelloWorld"); m.put("key1", 2); String s = (String) m.get("key"); System.out.println(s); System.out.println((String) m.get("key1")); } }
编译不出错,但是执行的时候报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Account.main(Account.java:11)
加入泛型机制之后的代码:
import java.util.Map; import java.util.HashMap; public class Account { public static void main(String[] args) throws InterruptedException { HashMap<String, String> m = new HashMap<String, String>(); m.put("key", "blarg"); m.put("key1", 2); String s = (String) m.get("key"); System.out.println(s); System.out.println((String) m.get("key1")); } }
编译之后会报错:
Account.java:8: cannot find symbol symbol : method put(java.lang.String,int) location: class java.util.HashMap<java.lang.String,java.lang.String> m.put("key1", 2); ^ 1 error
Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“String 到 列表”或者“String 到 String 的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作 ClassCastException 展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。
泛型还有一个附带好处,那就是消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。看明白了上面的内容那么回头看反省的基础就容易一些啦~
泛型基础
类型参数
在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。
泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是 java.util.Map 接口的定义的摘录:
public interface Map<K, V> { public void put(K key, V value); public V get(K key); }
Map 接口是由两个类型参数化的,这两个类型是键类型 K 和值类型 V。(不使用泛型)将会接收或返回 Object 的方法,现在使用具体的类来代替 K 或 V,这样就能起到更好的约束作用。
当声明或者实例化一个泛型的对象时,必须指定类型参数的值:
Map<String, String> map = new HashMap<String, String>();
注意,在本例中,必须指定两次类型参数。一次是在声明变量 map 的类型时,另一次是在 HashMap 类的实例化时。
编译器在遇到一个 Map<String, String> 类型的变量时,知道 K 和 V 现在被绑定为 String,因此它知道在这样的变量上调用 Map.get() 将会得到 String 类型。
除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。
一个简单的泛型类
此时,您可以开始编写简单的泛型类了。到目前为止,泛型类最常见的用例是容器类(比如集合框架)或者继承容器的类(比如 WeakReference 或 ThreadLocal)。我们来编写一个类,它类似于 List,他充当一个容器。其中,我们使用泛型来表示这样一个约束,即 Lhist 的所有元素将具有相同的类型。为了实现起来简单,Lhist 使用一个固定大小的数组来保存值,并且不接受 null 值。
Lhist 类将具有一个类型参数 V(该参数是 Lhist 中的值的类型),并将具有以下方法:
public class Lhist<V> { public Lhist(int capacity) { ... } public int size() { ... } public void add(V value) { ... } public void remove(V value) { ... } public V get(int index) { ... } }
要实例化 Lhist,只要在声明时指定类型参数和想要的容量:
Lhist<String> stringList = new Lhist<String>(10);
实现构造函数
在实现 Lhist 类时,您将会遇到的第一个拦路石是实现构造函数。您可能会像下面这样实现它:
public class Lhist<V> { private V[] array; public Lhist(int capacity) { array = new V[capacity]; //这是错误的 } }
这似乎是分配后备数组最自然的一种方式,但是不幸的是,您不能这样做。具体原因很复杂,当学习到底层细节一节中的“擦除”主题时,您就会明白。分配后备数组的实现方式很古怪且违反直觉。下面是构造函数的一种可能的实现(该实现使用集合类所采用的方法):
public class Lhist<V> { private V[] array; public Lhist(int capacity) { array = (V[]) new Object[capacity]; } }
另外,也可以使用反射来实例化数组。但是这样做需要给构造函数传递一个附加的参数 —— 一个类常量,比如 Foo.class。后面在 Class<T> 一节中将讨论类常量。
实现方法
实现 Lhist 的方法要容易得多。下面是 Lhist 类的完整实现:
public class Lhist<V> { private V[] array; private int size; public Lhist(int capacity) { array = (V[]) new Object[capacity]; } public void add(V value) { if (size == array.length) throw new IndexOutOfBoundsException(Integer.toString(size)); else if (value == null) throw new NullPointerException(); array[size++] = value; } public void remove(V value) { int removalCount = 0; for (int i=0; i<size; i++) { if (array[i].equals(value)) ++removalCount; else if (removalCount > 0) { array[i-removalCount] = array[i]; array[i] = null; } } size -= removalCount; } public int size() { return size; } public V get(int i) { if (i >= size) throw new IndexOutOfBoundsException(Integer.toString(i)); return array[i]; } }
注意,您在将会接受或返回 V 的方法中使用了形式类型参数 V,但是您一点也不知道 V 具有什么样的方法或域,因为这些对泛型代码是不可知的。
使用 Lhist 类
使用 Lhist 类很容易。要定义一个整数 Lhist,只需要在声明和构造函数中为类型参数提供一个实际值即可:
Lhist<Integer> li = new Lhist<Integer>(30);
编译器知道,li.get() 返回的任何值都将是 Integer 类型,并且它还强制传递给 li.add() 或 li.remove() 的任何东西都是 Integer。除了实现构造函数的方式很古怪之外,您不需要做任何十分特殊的事情以使 Lhist 是一个泛型类。