目录
1、为什么使用泛型?
2、什么是泛型类?如何定义一个泛型类?
泛型的命名约定
3、什么是泛型方法?如何定义一个泛型方法?
4、什么是有界类型参数?如何定义有界类型参数?
(1)多个边界的类型参数定义
(2)有界类型参数在泛型方法中的应用
5、如何区别泛型类和它的子类型?
所谓泛型,即参数化类型,目的是将具体类型参数化,在使用时需要传入具体类型进行替换。参数又分为实参和形参,泛型属于类型形参(好比抽象函数,是一种泛指,类似于数学函数中用 x,y,z 代表具体的值)。
1、为什么使用泛型?
泛型可以使类型在定义类、接口和方法的时侯成为参数,与方法中使用的形参非常相似,类型参数(泛型)提供了一种通过不同的输入而重用代码的方式。类型参数和形参的不同之处在于形参输入的是具体的值,而类型参数输入的是类型。
使用泛型可以在编译时检测到更多的错误,从而增加代码的稳定性。
使用泛型可以带来以下好处:
(1)在编译时拥有更强的类型检查。Java 编译器会对泛型进行强类型检查,并在代码违反类型安全的时后产生错误信息。一般来说,修复编译时错误比修复运行时的错误会更加容易,因为运行时错误往往更难以找到。
(2)消除类型之间的强制转换。例如,以下代码没有使用泛型就需要进行类型转换:
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); // no cast
(3)使程序员能够实现泛型算法。通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,这些算法可以自定义,并且类型安全,代码也更易于阅读。// 参考一些集合类的定义
2、什么是泛型类?如何定义一个泛型类?
泛型类是指通过对参数类型进行泛化的类或接口。下面的 Box.class 可以演示这个概念。首先,如果 Box.class 在不使用泛型的情况下是这样定义的:
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
该方法接受或返回 Object,所以除了基本的数据类型外,可以传入任何类型的对象。但是,当上边程序在进行编译时,没有办法去验证这个类是如何使用的。比如,一开始可能会往 Box 中放置一个Integer 对象,而接下来有可能会错误地传入一个String 对象,如果此时仍然期望从 Box 中获得一个 Integer 对象,会导致运行时错误。
泛型类的定义格式如下:
class name<T1, T2, ..., Tn> { /* ... */ }
类型参数部分,用尖括号(<>)进行分隔,并跟在类名后面。它指定的类型参数为 T1,T2,…,和 Tn。
如果要使用泛型更新 Box 类,可以通过将代码 “public class Box” 更改为 “public class Box<T>” 来创建泛型类声明。该方式引入了类型变量 T,该变量可以在类中的任何地方使用。
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
如你所见,所有出现的 Object 都被 T 取代,类型变量 T 可以是任何类型。同样的技术也可以应用于创建通用接口。下边是对于接口的声明示例:
泛型的命名约定
按照约定,泛型的名称是单个的大写字母。这与变量的命名约定形成了鲜明的对比,如果没有这些约定,就很难区分类型变量和普通类或接口命名之间的区别。
最常用的泛型名称如下:
- E - 代表元素(被Java集合框架广泛使用)
- K - 代表键 Key
- N - 代表数字类型
- T - Type(代表类型)
- V - 代表值 Value
- S,U,V etc. - 代表第二个,第三个,第四个类型
调用和实例化泛型
在 Java SE 7 及更高版本中,只要编译器能够从上下文确定或推断类型参数,就可以将调用构造函数所需的类型参数替换为类型参数的空集(<>)。例如,可以使用以下代码创建 Box<Integer> 的实例:// 类型推断,是Java编译器根据方法调用或方法声明来确定参数类型的能力。
Box<Integer> integerBox = new Box<>();
3、什么是泛型方法?如何定义一个泛型方法?
泛型方法是引入了类型参数的方法。类似于声明泛型类,但类型参数的作用域仅限于声明它的方法。Java 允许使用静态的和非静态的泛型方法,以及泛型构造函数。
泛型方法的声明包括一个类型参数集 <K,V>,该类型参数集位于尖括号内,出现在方法的返回类型之前。对于静态泛型方法,强制要求类型参数必须出现在方法的返回类型之前。
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);
boolean same = Util.compare(p1, p2);
一般来说,调用方法中的这个类型 <Integer, String> 可以省略,Java 编译器会推断出正确的类型。通过类型推断,允许将泛型方法作为普通方法调用,而不需要在尖括号内指定具体的类型。
4、什么是有界类型参数?如何定义有界类型参数?
有时候,可能想严格限制泛型的定义界限,例如,操作数字的方法只能接受 Number 或 Number 子类的实例,这时候就需要用到有界类型参数。
要声明一个有界类型参数,需要列出类型参数的名称,跟上 extends 关键字,然后再跟上它的上界(本例中是 Number)。// extends 限制了类型参数的上界
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
除了可以限制泛型的实例化范围外,有界类型参数还允许实例调用边界类型中定义的方法:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
在上述示例的 isEven() 方法中,n 可以调用定义在 Integer 类中的 intValue() 方法。
(1)多个边界的类型参数定义
上边的例子都是一个类型参数使用一个单一的边界,但是,Java 中一个类型参数可以有多个边界:
<T extends B1 & B2 & B3>
具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界定义的是类,那么必须首先指定它。例如:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
如果未首先指定继承 A.calss,那么会产生编译时错误:
class D <T extends B & A & C> { /* ... */ } // 编译错误
(2)有界类型参数在泛型方法中的应用
有界类型参数是实现泛型算法的关键。例如下面的方法,它计算数组 T[] 中大于指定元素 elem 的元素数量。
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // 编译错误
++count;
return count;
}
这个方法的实现虽然很简单,却不能通过编译,因为大于运算符 (>) 只能应用于基本数据类型,比如:short、int、double、long、float、byte 和 char。所以不能使用 > 操作符来比较一个对象。为了解决这个问题,可以尝试使用 Comparable<T> 接口进行替换:// 借助 Comparable 中的方法对对象进行比较
public interface Comparable<T> {
public int compareTo(T o);
}
更改后的代码如下,借助了有界类型参数,使用了边界类中定义的方法:
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
5、如何区别泛型类和它的子类型?
一般来说,只要类型兼容,就可以将一种类型的对象赋值给另一种类型的对象。例如,可以将一个 Integer 赋值给 Object,因为 Object 是 Integer 的超类型之一。对于泛型也是如此,比如调用一个泛型类,将 Number 作为它的类型参数,如果参数与 Number 兼容,则允许调用 add() 方法:
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<Number> 的参数。这又意味着什么呢?该方法是否允许传入 Box<Integer> 或 Box<Double>?最终的答案是 “否”,因为 Box<Integer> 和 Box<Double> 并不是 Box<Number> 的子类型。
任意给定两种具体类型 A 和 B,无论 A 和 B 之间是否有关系,MyClass<A> 和 MyClass<B> 都没有任何关系。因为 MyClass<A> 和 MyClass<B> 的共同父类是 Object。
所以,正确的做法是通过继承或实现泛型类或接口,然后对其进行子类型的划分。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends 和 implements 决定。
以 Collections 类为例,ArrayList<E> 实现了 List<E>, List<E> 扩展了 Collection<E>。所以ArrayList<String> 是 List<String> 的子类型,而 List<String> 是 Collection<String> 的子类型。所以只要不改变类型参数,类型之间的子类型关系就会保留。
至此,对于 Java 泛型的基本知识介绍完毕。