java 泛型
文章目录
为什么使用泛型?
在简单来说,泛型允许在定义类、接口和方法时将类型(类和接口)作为参数。类似于方法声明中使用的形式参数,类型参数为您提供了一种重用相同代码但使用不同输入的方式。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码相较于非泛型代码具有许多优势:
-
编译时更强的类型检查。
Java编译器会对泛型代码进行强类型检查,并在代码违反类型安全性时发出错误。修复编译时错误比修复运行时错误更容易,后者很难找到。 -
消除了类型转换。
在没有使用泛型的代码中,需要进行类型转换:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
当重写为使用泛型时,代码不需要进行类型转换:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // 无需转换
- 可以实现泛型算法。
使用泛型,程序员可以实现适用于不同类型集合的通用算法,这些算法可以进行定制,并且具有类型安全性和更易读的特点。
以上是使用泛型的一些好处。通过利用泛型,可以使代码更加类型安全、简洁且易于维护。
Generic Types(泛型)
泛型类型是参数化类型的通用类或接口。下面的Box类将被修改以演示这个概念。
一个简单的Box类
首先,看一下操作任何类型对象的非泛型Box类。它只需要提供两个方法:set,用于向盒子中添加对象;get,用于获取对象:
public class Box {
private Object object;
public void set(Object object) {
this.object = object; }
public Object get() {
return object; }
}
由于其方法接受或返回Object,你可以自由地传入任何你想要的内容,只要不是原始类型之一。在编译时,没有办法验证类的使用方式。代码的一部分可能会将Integer放入盒子中,并期望从中获取到整数,而代码的另一部分可能错误地传入了一个String,导致运行时错误。
Box类的泛型版本
泛型类的定义格式如下:
class name<T1, T2, ..., Tn> {
/* ... */ }
类型参数部分由尖括号(<>)包围,紧跟在类名后面。它指定了类型参数(也称为类型变量)T1、T2、…和Tn。
要将Box类更新为使用泛型,只需将代码"public class Box"更改为"public class Box",创建一个泛型类型声明。这引入了类型变量T,在类内部的任何地方都可以使用。
有了这个改变,Box类变成了:
/**
* Box类的泛型版本。
* @param <T> 被封装值的类型
*/
public class Box<T> {
// T代表“Type”
private T t;
public void set(T t) {
this.t = t; }
public T get() {
return t; }
}
可以看到,所有Object的出现都被替换为T。类型变量可以是你指定的任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。
这种技术同样适用于创建泛型接口。
类型参数命名约定
按照惯例,类型参数的名称是单个大写字母。这与你已经了解的变量命名约定形成鲜明对比,并且有充分的理由:如果没有这个约定,很难区分类型变量和普通的类或接口名称。
最常用的类型参数名称有:
- E - 元素(Java集合框架广泛使用)
- K - 键
- N - 数字
- T - 类型
- V - 值
- S, U, V等 - 第2个、第3个、第4个类型
你会在Java SE API以及本课程的其余部分中看到这些名称的使用。
调用和实例化泛型类型
要在代码中引用泛型Box类,必须进行泛型类型调用,将T替换为某个具体的值,例如Integer:
Box<Integer> integerBox;
可以将泛型类型调用视为普通方法调用的类比,但是不是将参数传递给方法,而是将类型参数(在本例中为Integer)传递给Box类本身。
类型参数和类型参数的术语:许多开发人员在使用过程中将“类型参数”和“类型参数”这两个术语互换使用,但这些术语并不相同。在编码时,通过提供类型参数来创建参数化类型。因此,Foo中的T是类型参数,Foo f中的String是类型参数。本课程在使用这些术语时遵守这个定义。
与任何其他变量声明一样,这段代码实际上并没有创建一个新的Box对象。它只是声明integerBox将保存对"Box of Integer"的引用,这是如何读取Box的含义。
泛型类型的调用通常称为参数化类型。
要实例化这个类,像平常一样使用new关键字,但是在类名和括号之间加上:
Box<Integer> integerBox = new Box<Integer>();
菱形操作符
在Java SE 7及更高版本中,你可以用一组空的类型参数(<>)替代调用泛型类构造函数所需的类型参数,只要编译器能够从上下文中确定或推断出这些类型参数。这对尖括号(<>)被非正式地称为菱形。例如,你可以使用以下语句创建Box的实例:
Box<Integer> integerBox = new Box<>();
有关菱形符号和类型推断的更多信息,请参见类型推断。
多个类型参数
如前所述,泛型类可以有多个类型参数。例如,泛型OrderedPair类实现了泛型Pair接口:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key; }
public V getValue() {
return value; }
}
下面的语句创建了两个OrderedPair类的实例:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
代码new OrderedPair<String, Integer>将K实例化为String,V实例化为Integer。因此,OrderedPair的构造函数的参数类型分别是String和Integer。由于自动装箱,将String和int传递给这个类是有效的。
如菱形符号中所述,由于Java编译器可以从声明OrderedPair<String, Integer>中推断出K和V的类型,可以使用菱形符号简化这些语句:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
要创建一个泛型接口,遵循与创建泛型类相同的约定。
参数化类型
你还可以用参数化类型(例如List)替换类型参数(即K或V)。例如,使用OrderedPair<K, V>示例:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
原始类型(Raw Types)
原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型Box类:
public class Box<T> {
public void set(T t) {
/* ... */ }
// ...
}
要创建Box的参数化类型,你需要为形式类型参数T提供实际类型参数:
Box<Integer> intBox = new Box<>();
如果省略实际类型参数,就会创建Box的原始类型:
Box rawBox = new Box();
因此,Box是泛型类型Box的原始类型。然而,非泛型的类或接口类型不是原始类型。
原始类型在旧代码中出现,因为许多API类(如Collections类)在JDK 5.0之前都不是泛型的。当使用原始类型时,实际上获得的是泛型之前的行为——一个Box会给你Objects。出于向后兼容性的考虑,将参数化类型赋值给其原始类型是被允许的:
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)
这个警告显示原始类型绕过了泛型类型检查,将不安全的代码检查推迟到运行时。因此,应该避免使用原始类型。
“类型擦除”一节中详细介绍了Java编译器如何使用原始类型。
未检查的错误消息
正如前面提到的,在将旧代码与泛型代码混合使用时,你可能会遇到类似以下的警告消息:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
当使用操作原始类型的旧API时,就会出现这种情况,如下面的例子所示:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
术语"unchecked"表示编译器没有足够的类型信息来执行所有必要的类型检查,以确保类型安全性。默认情况下,“unchecked”警告被禁用,但编译器给出了提示。要查看所有的“unchecked”警告,请使用-Xlint:unchecked重新编译。
使用-Xlint:unchecked重新编译前面的示例,会显示以下附加信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
要完全禁用未检查的警告,请使用-Xlint:-unchecked标志。@SuppressWarnings(“unchecked”)注解可以抑制未检查的警告。如果对@SuppressWarnings语法不熟悉,请参阅注解。
泛型方法(Generic Methods)
泛型方法是引入自己的类型参数的方法。这类似于声明一个泛型类型,但类型参数的范围仅限于声明它的方法内部。静态和非静态的泛型方法都是允许的,还可以有泛型类构造函数。
泛型方法的语法包括一个类型参数列表,在方法的返回类型之前以尖括号内的形式出现。对于静态的泛型方法,类型参数部分必须出现在方法的返回类型之前。
Util类包含一个泛型方法compare,用于比较两个Pair对象:
public class Util {
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());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) {
this.key = key; }
public void setValue(V value) {
this.value = value; }
public K getKey() {
return key; }
public V getValue() {
return value; }
}
调用该方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
类型已经被明确指定,如加粗所示。一般来说,可以省略这部分,编译器会推断需要的类型:
Pair<Integer, String> p1 = new Pair<>(1, "apple")

本文围绕Java泛型展开,介绍了使用泛型的好处,如编译时强类型检查、消除类型转换等。详细阐述了泛型类型、方法、有界类型参数等概念,还讲解了类型擦除的原理及影响。此外,指出了泛型在实例化、创建数组等方面存在的限制。
最低0.47元/天 解锁文章
1912

被折叠的 条评论
为什么被折叠?



