泛型是JDK1.5引入的新特性,简而言之就是将原来的具体类型进行参数化,也就是将所操作的数据类型指定为一个参数,在真正使用时传入具体的类型。这种参数可以在类、接口或方法的创建中使用,分别称之为泛型类、泛型接口和泛型方法。例如:
// 泛型接口
public interface List<E> extends Collection<E> {
}
// 泛型类
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}
// 泛型方法
public static <T> Stream<T> stream(T[] array) {
return stream(array, 0, array.length);
}
为什么使用泛型
1. 编译期间强类型检查
Java编译器会对代码进行类型检查,如果代码违反类型安全就会报错。编译期错误比运行期错误更容易修复。
注意:泛型只在编译期间有效,在运行期间会进行类型擦除,所以运行期间是没有泛型的。
2. 消除类型转换
在JDK1.5之前,Collection集合的参数类型为Object,这样就会导致获取数据是需要类型转换。而类型转换很容易触发ClassCastException
异常。但是使用了泛型之后,就不存在类型转换问题了:
// 需要类型转换
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
// 不需要类型转换
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
3. 实现通用的算法,减少代码量和异常率
通过使用泛型,开发者可以实现适用于不同类型集合的泛型算法,这些算法可以自定义,类型安全且易于阅读。
泛型类(Generic Types)
泛型类型指的是一个参数化类型的类或接口。
对于类型参数的命令,通常使用一个单大写字母来进行表示:
E - Element K - Key N - Number T - Type V -Value
定义一个泛型类:
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
实例化该泛型:
Box<Integer> integerBox = new Box<Integer>();
在JDK1.7之后添加了泛型的类型推导,JDK1.7之后就写成了一下的形式:
Box<Integer> integerBox = new Box<>();
原生类(RawTypes)
原生类型指的是泛型类去掉泛型参数部分的名称。例如:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
为了创建一个参数化类型BoxT
提供一个真实类型:
Box<Integer> intBox = new Box<>();
但是如果真实的参数类型被省略了,创建的就是原生类型:
Box rawBox = new Box();
因此,Box是泛型类Box
注意:非泛型类是不存在原生类型的。
原生类型出现在遗留代码中,因为在JDK1.5之前,许多API类(如Collections类)不是泛型的。当使用原生类型时,得到了是一个预泛型行为—Box类得到Object对象。为了向后兼容,允许将参数化类型赋值给其原生类型**:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
但是如果将一个原生类型赋值给参数化类型的话,会产生一个警告:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
如果使用一个原生类型去触发一个定义在其泛型类中的泛型方法,也会出现一个警告:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
未检查错误信息(Unchecked Error Message)
**
如前所述,在将遗留代码与泛型代码混合时,可能会遇到类似于以下内容的警告消息:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
unchecked
表示编译器没有足够的类型信息去保证类型安全。
泛型方法(Generic Methods)
泛型方法是引入自己类型参数的方法。类型参数的作用域仅限于声明它的方法。静态和非静态方法都可以声明为泛型方法,泛型类构造函数也可以。
泛型方法的语法**包括尖括号内的类型参数列表,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。**例如:
// 参数列表必须出现在返回值之前
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
}
有界类型参数(Bounded Type Parameters)
有时候需要限制可在参数化类型中用作类型参数的类型。例如,对数字进行操作的方法可能只希望接受数字或其子类的实例。这就是有界类型参数的用途。
若要声明有界类型参数,请列出类型参数的名称,后跟extends关键字,再后跟其上限(在本例中为Number)。
注意,这里的extends
在一般意义上是指Java类中的extends
或implements
**
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
多边界(Multiple Bounds)
泛型参数也可以存在多个边界:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
注意:如果其中一个边界是类,那么就需要排在泛型参数列表的第一位,否则将导致异常。
class D <T extends B & A & C> { /* ... */ } // compile-time error
A是一个class,必须排在第一个位置。
泛型、继承与子类型(Generic、Inheritance、andSubtypes)
将一种类型的对象赋值给另一种类型的对象是可以的,前提是两个对象之间是类型兼容的。例如,可以将一个Integer类型的对象赋值给Object类型的对象,因为Integer是Object的子类。
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在面向对象的术语中,称之为” is a” 的关系。因为Integer是一种Object,所以是可以被赋值的。但是Integer同样也是Number类型,故而下面的代码也是合法的:
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
这种关系应用到泛型中也是可行的:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
但是,对于下面这个方法来说,我们可以传递什么样的参数呢?
public void boxTest(Box<Number> n) { /* ... */ }
该方法接收一个类型为Box
使用泛型时,这个知识点是非常迷惑人的,但也确实是很重要的。
注意:假设存在两个类型A和B,尽管A/B之间可能存在关联,但是Class和Class是不存在任何关系的。
泛型类的子类型(Generic Classes and Subtyping)
开发者可以通过继承或实现去子类化一个泛型类。两者之间的类型参数关系是通过extends
和implements
来决定的。例如:ArrayList
注意:父类子类的关系都是通过extends或implements关键字来实现的,Box
**
通配符(Wildcards)
在泛型中使用的?
称之为通配符,表示一个未知的类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时也可作为返回类型。但是,通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。
上界通配符
格式如下:
? extends Any
真实参数只能是Any或其子类。其元素可以直接当做Any类型类访问。
public static void process(List<? extends Any> list) {
for (Any elem : list) {
// ...
}
}
无界通配符
无界通配符类型直接使用了通配符符号?
。例如List<?>.这调用了一个位置类型的集合。无界通配符使用的场景如下:
- 正在编码的method可以使用Object类中提供的方法
- 当代码在泛型类中使用不依赖于类型参数的方法时。例如,List.size或者List.clear. 事实上,Class<?>之所以经常使用,是因为Class
中的大多数方法都不依赖于T。
思考下面的printList方法:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
方法的目标是打印一个任意类型的集合,但是现在的实现却打印的是Object实例。这个方法不能打印List