扒一拔:Java 中的泛型(一)
目录
@
1 泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
1.1 为什么需要泛型
泛型是JDK1.5才出来的, 在泛型没出来之前, 我们可以看看集合框架中的类都是怎么样的。
以下为JDK1.4.2的 HashMap
可以看到, 在该版本中, 参数和返回值(引用类型)的都是 Object
对象。 而在 Java 中, 所有的类都是 Object
子类, 实用时, 可能需要进行强制类型转换。 这种转换在编译阶段并不会提示有什么错误, 因此, 在使用时, 难免会出错。
而有了泛型之后, HashMap
的中使用泛型来进行类型的检查
通过泛型, 我们可以传入相同的参数又能返回相同的参数, 由编译器为我们来进行这些检查。
这样可以减少很多无关代码的书写。
因此, 泛型可以使得类型参数化, 泛型有如下的好处
- 类型参数化, 实现代码的复用
- 强制类型检查, 保证了类型安全,可以在编译时就发现代码问题, 而不是到在运行时才发现错误
- 不需要进行强制转换。
1.2 类型参数命名规约
按照惯例,类型参数名称是单个大写字母。 通过规约, 我们可以容易区分出类型变量和普通类、接口。
- E – 元素
- T – 类型
- N – 数字
- K – 键
- V – 值
- S,U,V – 第2种类型, 第3种类型, 第4种类型
2 泛型的简单实用
2.1 最基本最常用
最早接触的泛型, 应该就是集合框架中的泛型了。
List<Integer> list = new ArrayList<Integer>();
list.add(100086); //OK
list.add("Number"); //编译错误
在以上的例子中, 将 String
加入时, 会提示错误。 编译器不会编译通过, 从而保证了类型安全。
2.2 简单泛型类
2.2.1 非泛型类
先来定义一个简单的类
public class SimpleClass {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
这么写是没问题的。 但是在使用上可能出现如下的错误:
public static void main(String[] args) {
SimpleClass simpleClass = new SimpleClass();
simpleClass.setObj("ABC");// 传入 String 类型
Integer a = (Integer) simpleClass.getObj(); // Integer 类型接受
}
以上写是不会报错的, 但是在运行时会出现报错
java.lang.ClassCastException
如果是一个人使用, 那确实有可能会避免类似的情况。 但是, 如果是多人使用, 则你不能保证别人的用法是对的。 其存在着隐患。
2.2.2 泛型类的定义
我们可以使用泛型来强制类型限定
public class GenericClass<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
2.2.3 泛型类的使用
在使用时, 在类的后面, 使用尖括号指明参数的类型就可以
@Test
public void testGenericClass(){
GenericClass<String> genericClass = new GenericClass<>();
genericClass.setObj("AACC");
/* Integer str = genericClass.getObj();//*/
}
如果类型不符, 则编译器会帮我们发现错误, 导致编译不通过。
2.3 简单泛型接口
2.3.1 定义
与类相似, 以 JDK 中的 Comparable
接口为例
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
2.3.2 实现
在实现时, 指定具体的参数类型即可。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
...
}
2.4 简单泛型方法
泛型方法可以引入自己的参数类型, 如同声明泛型类一样, 但是其类型参数我的范围只是在声明的方法本身。 静态方法和非静态方法, 以及构造函数都可以使用泛型。
2.4.1 泛型方法声明
泛型方法的声明, 类型变量放在修饰符之后, 在返回值之前
public class EqualMethodClass {
public static <T> boolean equals(T t1, T t2){
return t1.equals(t2);
}
}
如上所示, 其中 是不能省略的。 而且可以是多种类型, 如 <K, V>
public class Util {
public static <K, V> boolean sameType(K k, V v) {
return k.getClass().equals(v.getClass());
}
}
2.4.2 泛型方法的调用
调用时, 在方法之前指定参数的类型
@Test
public void equalsMethod(){
boolean same = EqualMethodClass.<Integer>equals(1,1);
System.out.println(same);
}
3 类型变量边界
3.1 定义
如果我们需要指定类型是某个类(接口)的子类(接口)
<T extends BundingType>
使用 extends
, 表示 T
是 BundingType
的子类, 两者都可以是类或接口。
此处的 extends
和继承中的是不一样的。
如果有多个边界限定:
<T extends Number & Comparable>
使用的是 &
符号。
注意事项
如果边界类型中有类, 则类必须是放在第一个
也就是说
<T extends Comparable & Number> // 编译错误
会报错
3.2 示例
有时, 我们需要对类型进行一些限定, 比如说, 我们要获取数组的最小元素
public class ArrayUtils {
public static <T> T min(T[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
T smallest = arr[0];
for (int i = 0; i < arr.length; i++) {
if (smallest.compareTo(arr[i]) > 0) {
smallest = arr[i];
}
}
return smallest;
}
}
上面的是报错的。 因为, 在该函数中, 我们需要使用 compareTo
函数, 但是, 并不是所欲的类都有这个函数的。 因此, 我们可以这样子限定
将 转换成 <T extends Comparable
即可。
测试
@Test
public void testMin() {
Integer a[] = {1, 4, 5, 6, 0, 2, -1};
Assertions.assertEquals(ArrayUtils.<Integer>min(a), Integer.valueOf(-1));
}
4 泛型, 继承和子类型
4.1 泛型和继承
在 Java 继承中, 如果变量 A 是 变量 B 的子类, 则我们可以将 A 赋值给 B。 但是, 在泛型中则不能进行类似的赋值。
对继承来说, 我们可以这样做
public class Box<T> {
List<T> boxs = new ArrayList<>();
public void add(T element) {
boxs.add(element);
}
public static void main(String[] args) {
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
}
}
但是, 在泛型中, Box
不能赋值给 Box
(即两个不是子类或父类的关系)。
可以使用下图来进行阐释
注意:
对于给定的具体类型 A 和 B(如 Number 和 Integer),
MyClass
与MyClass
没有任何的关系, 不管 A 和 B 之间是否有关系。
4.2 泛型和子类型
在 Java 中, 我们可以通过继承或实现来获得一个子类型。 以 Collection
为例
由于 ArrayList
实现了 List
, 而 List
继承了Collection
。 因此, 只要类型参数没有更改(如都是 String 或 都是 Integer), 则类型之间子父类关系会一直保留。
5 类型推断
类型推断并不是什么高大上的东西, 我们日常中其实一直在用到。它是 Java 编译器的能力, 其查看每个方法调用和相应声明来决定类型参数, 以便调用时兼容。
值得注意的是, 类型推断算法仅仅是在调用参数, 目标类型和明显的预期返回类型时使用。
5.1 类型推断和泛型方法
在下面的泛型方法中
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
public class BoxDemo {
public static <U> void addBox(U u,
List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
ArrayList<Box<Integer>> listOfIntegerBoxes =
new ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
输出
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
我们可以看到, 泛型方法 addBox 中定义了一个类型参数 U, 在泛型方法的调用时, Java 编译器可以推断出该类型参数。 因此, 很多时候, 我们不需要指定他们。
如上面的例子, 我们可以显示的指出
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
也可以省略, 这样, Java 编译器可以从方法参数中推断出
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
由于方法参数是 Integer, 因此, 可以推断出类型参数就是 Integer。
5.2 泛型类的类型推断和实例化
这是我们最常用到的类型推断了: 将构造函数中的类型参数替换成<>
(该符号被称为“菱形(The diamond)”), 编译器可以从上下文中推断出该类型参数。
比如说, 正常情况先, 我们是这样子声明的
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
但是, 实际上, 构造函数的类型参数是可以推断出来的。 因此, 这样子写即可
Map<String, List<String>> myMap = new HashMap<>();
但是, 不能将 <>
去掉, 否则编译器会报警告。
Map<String, List<String>> myMap = new HashMap(); // 警告
5.3 类的类型推断和构造函数
在泛型类和非泛型类中, 构造函数都是可以声明自己的类型参数的。
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass<Integer> myObject = new MyClass<>("");
}
}
在以上代码 main 函数中, X
对应的类型是 Integer
, 而 T
对应的类型是 String
。
那么, 菱形 <>
对应的是 X
还是 T
呢?
在 Java SE 7 之前, 其对应的是构造函数的类型参数。 而在 Java SE 7及以后, 其对应的是类的类型参数。
也就是说, 如果类不是泛型, 则代码是这样子写的
class MyClass{
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass myObject = new MyClass("");
}
}
T
的实际类型, 编译器根据方法的参数推断出来。
5.4 类型推断和目标类型
Java 编译器利用目标类型来推断泛型方法调用的类型参数。 表达式的目标类型就是 Java 编译器所期望的数据类型, 根据该数据类型, 我们可以推断出泛型方法的类型。
以 Collections
中的方法为例
static <T> List<T> emptyList();
我们在赋值时, 是这样子
List<String> listOne = Collections.emptyList();
该表达式想要得到 List
的实例, 那么, 该数据类型就是目标类型。 由于 emptyList
的返回值是 List
, 因此, 编译器就推断, T
对应的实际类型就是 String
。
当然, 我们也可以显示的指定该类型参数
List<String> listOne = Collections.<String>emptyList();
6 通配符
在泛型中, 使用 ?
作为通配符, 其代表的是未知的类型。
6.1 设定通配符的下限
有时候, 我们想写一个方法, 它可以传递 List
, List
和List
。 此时, 可以使用通配符来帮助我们了。
设定通配符的上限
使用?
, 其后跟随着 extends
, 再后面是 BundingType
(即上边界)
<? extends BundingType>
示例
class MyClass{
public static void process(List<? extends Number> list) {
for (Number elem : list) {
System.out.println(elem.getClass().getName());
}
}
public static void main(String[] args) {
List<Integer> integers = new LinkedList<>(Arrays.asList(1));
List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
List<Number> numbers = new LinkedList<>(Arrays.asList(1));
process(integers);
process(doubles);
process(numbers);
}
}
输出
java.lang.Integer
java.lang.Double
java.lang.Integer
也就是说, 我们通过通配符, 可以将List
, List
和List
作为参数传递到同一个函数中。
6.2 设定通配符的下限
上限通配符是限定了参数的类型是指定的类型或者是其子类, 使用 extends
来进行。
而下限通配符, 使用的是 super
关键字, 限定了未知的类型是指定的类型或者其父类。
设定通配符的下限
<? super bundingType>
在 ?
后跟着 super
, 在跟上对应的边界类型。
示例
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
对于该方法, 由于我们是要将整型添加到列表中, 因此, 需要传入的列表必须是整型或者其父类。
6.3 未限定的通配符
当然, 我们也可以使用未限定的通配符。 如List<?>
, 表示未知类型的列表。
使用通配符的情景
- 所写的方法需要使用 Object 类所提供的功能
- 所写的方法, 不依赖于具体的类型参数。 比较常见的是反射中, 用
Class<?>
而非Class
, 因为绝大部分方法都不依赖于具体的类型。
那么, 为什么不使用 List