在我的学生时代,Java泛型在我眼里是一个非常难的知识点,看了许多相关的文章,最后都没能学好它。现在回头看,当时犯了两个错:
- 看的文章不成体系,东讲一点,西讲一点;
- 死记硬背,没有理解好。
现在工作了,看到了Java官方教程中关于泛型的部分,相见恨晚。讲得真的很好,它用基础简单的例子,成体系的介绍了泛型的使用方法以及简单的原理。推荐英文好的同学可以看看:Lesson: Generics。
我写的这篇博客,可以说是Java官方教程的读后笔记吧。如果有不理解或觉得我说得不准确的,欢迎留言或私信交流,我们共同进步。
本系列文章:
Java泛型其实很简单(二):泛型,继承,子类型
文章目录
1 什么是泛型?为何我们需要泛型?
假设我们需要设计一个“箱子”,它可以存放一类“物品”,但是“物品”的具体类型是什么,我们并不清楚,我们只知道它可以存放一类“物品”。那么,我们要怎么设计“箱子”这个类呢?
1.1 不使用泛型的实现
public class Box {
private Object item;
private void setItem(Object item) {
this.item = item;
}
private Object getItem() {
return item;
}
}
这样子做会有危险,当我们我们希望一个“箱子只能存取Integer
类型时,如果有人往里面存放了String
类型,编译器并不会报错。
Box box = new Box();
box.setItem("StringItem");
Integer item = (Integer)box.getItem(); // RuntimeError:java.lang.ClassCastException
由于Java编译器并不知道box
里面存的到底是什么东西,它允许你存入任何的Object
对象,但,这并不是我们所期望的结果,所以虽然程序通过了编译,却会在运行时抛出ClassCastException
。
1.2 使用泛型的实现
public class GenericBox<T> {
private T item;
private void setItem(T item) {
this.item = item;
}
private T getItem() {
return item;
}
public static void main(String[] args) {
GenericBox<Integer> box = new GenericBox<Integer>();
box.setItem("StringItem"); // compile error
}
}
在我们使用泛型以后,我们可以限制box
只能存放入Integer
类型的对象,如果存放的不是Integer
,编译时便会报错:java: incompatible types: java.lang.String cannot be converted to java.lang.Integer
。
泛型的出现,主要给我们带来三个好处:
- 编译时类型检查:在编译时,编译器便会根据泛型检查你的类型是否正确,这样子比在运行时报错更容易解决问题;
- 消除类型转换:例如
Box box = new Box();
box.setItem("item");
String item = (String)box.getItem(); // 不使用泛型,需要类型转换
GenericBox<String> box = new GenericBox<String>();
box.setItem("item");
String item = box.getItem(); // 使用泛型,无需类型转换
- 可以实现泛型相关程序:你可以实现一个泛型方法,对一个集合的对象进行某种操作,无需担心类型安全,简单易懂。
2 泛型类
2.1 如何定义泛型类?
我们上面所使用的的GenericBox
,就是一个泛型类。泛型类的格式如下:
class GenericClassName<T1,T2, ..., Tn>
和非泛型的类的签名不同的是,后面多个:<T1,T2, ..., Tn>
,这些T表示的是类型参数,你可以在这个类中,使用T表示某种特定类型。我们再次使用GenericBox
来做解释:
public class GenericBox<T> {
private T item;
private void setItem(T item) {
this.item = item;
}
private T getItem() {
return item;
}
public static void main(String[] args) {
GenericBox<String> box = new GenericBox<String>();
box.setItem("StringItem");
}
}
GenericBox
只有一个泛型参数,所以它的类签名为:public class GenericBox<T>
,类型参数T可以表示某种特定类型,我们用在了成员变量item
,以及setItem()
上。
2.2 如何初始化泛型类
GenericBox<String> box = new GenericBox<String>();
这样子,我们便初始化了一个泛型对象box
,我们将T指定为String
类型,这个box
的类可以理解为:
public class GenericBox {
private String item;
private void setItem(String item) {
this.item = item;
}
private String getItem() {
return item;
}
}
2.2.1 常用的类型参数表示方法
通过上面我们知道,放在尖括号“<>”里的我们称为类型参数。类型参数我们一般用全大写的英文字母表示,例如:
- T:表示类型
- E:表示元素
- N:表示数字
当然,你也可以使用E或者e表示数字,就像你可以将类名小写。
2.2.2 多个类型参数
一个泛型类可以有多个类型参数,例如:
public class Pair<K, V> {
private K key;
private V value;
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<String, Integer>();
String key = pair.getKey();
Integer value = pair.getValue();
}
}
典型的用法有jdk源码的:Map<K, V>
。
3 泛型接口
3.1 如何定义泛型接口
定义泛型接口与定义泛型类相似:interface InterfaceName<T1, T2, ..., Tn>
3.2 如何实现泛型接口
public interface Pair<K, V> {
void setKey(K key);
void setValue(V v);
K getKey();
V getValue();
}
3.2.1 实现接口时,指定类型参数的参数类型:
public class PairImpl implements Pair<String, Integer> {
private String key;
private Integer value;
@Override
public void setKey(String key) {
this.key = key;
}
@Override
public void setValue(Integer value) {
this.value = value;
}
@Override
public String getKey() {
return key;
}
@Override
public Integer getValue() {
return value;
}
}
这样子实现的话,PairImpl
只能存放String
类型的key
,Integer
类型的value
,不够灵活。
3.2.2 使用泛型类实现
public class GenericPairImpl<K, V> implements Pair<K, V> {
private K key;
private V value;
@Override
public void setKey(K key) {
this.key = key;
}
@Override
public void setValue(V value) {
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
}
这样子我们就能通过初始化泛型类的时候指定key
和value
的类型。
4 泛型方法
4.1 如何定义泛型方法
在我们上面所举的例子中:public V getValue()
是一个泛型方法吗?
答案:不是。它只是泛型类中的一个方法,这个方法无法决定它的类型参数是什么(初始化对象的时候就决定好了)。
定义一个泛型方法:
public class GenericMethod {
public <T> boolean compare(T t1, T t2) {
return t1.equals(t2);
}
}
类型参数要写在返回值前。这时我们调用这个方法时,就可以指定我们想要的类型了。
GenericMethod object = new GenericMethod();
object.<Integer>compare(1, 2);
object.<Double>compare(1.0, 2.0);