通过阅读完本篇文章,你将知道:
- 什么是泛型?
- 为什么要有泛型?
- 使用泛型的正确姿势?
- 如何对泛型参数进行限定?
- 泛型有哪些局限性或者细节需要注意?
什么是泛型
"泛型"的字面意思就是广泛的类型。它将Java类处理的数据类型进行参数化 ,由使用者传入 ,使得Java类与其操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型,这样,不仅可以复用代码,降低耦合 ,而且可以提高代码的可读性和安全性。
慢慢体会最后一句话:"提高了可读性和安全性"。
阅读完本篇文章,细细消化完之后你就能够理解:(1)为什么提高了可读性? (2)又为什么提高了安全性?
简单泛型使用案例:
java
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
Pair就是一个泛型类,与普通类的区别体现在:1)类名后面多了一个< T >;2)first和second的类型都是T。T是什么呢?T表示类型参数,它可以由使用者传入。
使用:
java
Pair<Integer> minmax = new Pair<Integer>(1,100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
为什么要有泛型
我们在第一小节定义了一个泛型类Pair,使用泛型参数T来表示Pair处理的数据类型是"广泛"的,可以由使用者传入。
问:不使用泛型,直接定义一个普通类,内部要处理的数据直接使用Object定义不行吗?
比如下面我Pair类直接这样写:
java
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
使用Pair的代码可以为:
java
Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();
Pair kv = new Pair("name", "老马");
String key = (String)kv.getFirst();
String value = (String)kv.getSecond();
这样写是可以的!并且,Java泛型的内部原理就是这样的!
稍微简单解释下基本实现原理:
我们知道,Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型类,
- Java编译器会将泛型代码转换为普通的非泛型代码 ,就像上面的普通Pair类代码及其使用代码一样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。
- Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。
再强调一下,Java泛型是通过擦除实现的 ,类定义中的类型参数如T会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数,比如Pair,运行中只知道Pair,而不知道Integer。认识到这一点是非常重要的,它有助于我们理解Java泛型的很多限制。 继续往后面看。
到这里我们已经知道了当我们在代码中使用泛型时,Java编译器实际上会把所有的泛型参数擦除掉,然后根据代码推断插入一些必要的强制类型转换,我们最终的泛型代码都变成了普通的非泛型代码。
那既然这样,为什么要使用泛型呢???
语言和程序设计的一个重要目标是将bug尽量消灭在摇篮里,能消灭在写代码的时候,就不要等到代码写完程序运行的时候。
只使用Object,代码写错的时候,开发环境和编译器不能帮我们发现问题,看代码:
java
Pair pair = new Pair("老马",1);
Integer id = (Integer)pair.getFirst();
String name = (String)pair.getSecond();
看出问题了吗?写代码时不小心把类型弄错了,不过,代码编译时是没有任何问题的,但运行时程序抛出了类型转换异常ClassCastException。
如果使用泛型,则不可能犯这个错误(不可能等到程序运行时才发现代码存在类型转换的错误),比如下面的代码:
java
Pair<String, Integer> pair = new Pair<>("老马",1);
Integer id = pair.getFirst(); //有编译错误
String name = pair.getSecond(); //有编译错误
所以,我们现在明白Java中设计泛型这一语法的目的是什么了吧 ,就是尽量提高程序的安全性,什么安全性?类型安全性。也即使用了泛型,如果在编译期间没有报错,那么基本可以保证程序在运行的时候不会出现类型安全问题。除了提高了程序的安全性,还有什么好处?可读性,怎么理解,我们可以发现使用了泛型之后就不用我们自己去手动做一些类型转换了,大大的提高了代码的简洁和可读性。
读到这里就基本知道了使用泛型的好处,那下一步就是要知道如何正确使用泛型。
使用泛型的正确姿势
在准备学习使用泛型之前,我们的脑海中应该对泛型的使用有一个基本的认识框架,也就是泛型可以在什么地方使用?
- 泛型可以用于定义类
- 泛型可以用于定义方法
- 泛型可以用于定义接口
接下来我们就按照上面的思路进行学习使用泛型。
定义泛型类
定义一个泛型类就是在直接类名添加泛型参数,然后在类的内部就可以使用这个泛型了。
java
class className <T> {
private T a;
public className(T a) {
this.a = a;
}
public T getA() {
return this.a;
}
public void setA(T a) {
this.a = a;
}
}
// 也可以定义多个泛型
class className<T, R, E...> {
}
定义泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系,定义一个泛型方法直接在方法的返回值前面添加泛型参数即可,然后就可以在方法内部 、方法的参数 、方法的返回值中使用泛型了
java
public static <T> int indexOf(T[] arr, T elm){
for(int i=0; i<arr.length; i++){
if(arr[i].equals(elm)){
return i;
}
}
return -1;
}
这个方法就是一个泛型方法,类型参数为T,放在返回值前面,它可以如下调用:
java
indexOf(new Integer[]{1,3,5}, 10)
也可以如下调用:
java
indexOf(new String[]{"hello", "老马", "编程"}, "老马")
与泛型类一样,类型参数可以有多个,以逗号分隔,比如:
java
public static <U, V> Pair<U, V> makePair(U first, V second){
Pair<U, V> pair = new Pair<>(first, second);
return pair;
}
然后可以如下调用:
java
makePair(1, "老马");
通过上面几个例子,我们可以发现:调用泛型方法时可以不用指定类型参数的实际类型,因为Java编译器可以根据在调用方法传的参数自动推断出来。
问:一般在什么时候使用泛型方法?
- 方法是有参数的(如果没有参数,就毫无意义,因为编译器就是通过在调用方法时传入的参数类型来确定整个方法中所有泛型参数具体的类型)
- 方法内的操作逻辑和具体的方法参数类型没有什么关系,使用泛型可以很好的限制方法中的操作逻辑,避免出现类型问题
- 或者方法的返回类型和方法传入参数有关系。
定义泛型接口
定义泛型接口和定义泛型类一样,也是直接在接口名后面添加泛型参数,然后在接口中就可以使用这个泛型了。 例如:
java
public interface Comparable<T> {
public int compareTo(T o);
}
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
与前面一样,T是类型参数。实现接口时,应该指定具体的类型,比如,对Integer类,实现代码是:
java
public final class Integer extends Number implements Comparable<Integer>{
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
//其他代码
}
泛型参数的限定
Java中泛型参数的限定其实就是对泛型参数的上界和下界进行一个限定,使得方法或接口或类更加灵活,更加灵活的读写。
留意上面一句话:"更加灵活的读写"。着重体会为什么对参数限定后可以更加灵活的读写?
首先,Java对泛型参数的限定提供了三种方法:
(1)< T extends E> :泛型参数必须是E或者E的子类,这个E可以是某个具体的类或者某个具体的接口,也可以是其它类型参数
(2)<? super E>:泛型参数必须是E或者E的父类,同样这个E可以是某个具体的类或者某个具体的接口,也可以是其他类型参数
(3)<?>:表示无限定通配符,通俗的讲可以是任何的类型。它的真正本意是:类型安全无知,为了保证类型安全,只能读不能写。
知道了对泛型参数限定的三种方法之后,就需要知道在什么场景下会使用。
< T extends E>的使用
使用场景:当我们需要对泛型参数进行更加灵活或者具体的读取时 ,我们就可以使用< T extends E>指定泛型参数的上界是E,那么我们就可以统一把传进来的具体泛型参数当做E类型进行读取了。
1. 上界是一个具体的类:
定义一个子类NumberPair,限定两个类型参数必须为Number或者Number的子类,那么我们就可以把传进来的泛型参数当做Number类型进行统一处理了,可以直接使用Number中的"读方法"了。
java
public class NumberPair<U extends Number, V extends Number>
extends Pair<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
}
例如下面我们可以在类中定义一个求和方法,直接使用Number中的doubleValue方法。
java
public double sum(){
return getFirst().doubleValue() +getSecond().doubleValue();
}
使用:
jaav
NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.sum();
限定类型后,如果类型使用错误,编译器会提示。
问:在前面中,我们知道在编译期间泛型会被擦除全部转换成Object类型,那指定边界之后泛型擦除还是全都转换成Object吗?
指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型,这也是容易理解的。
2. 上界是一个接口:
当我们限定类型参数的的上界是某个接口时,也即要求将来传入的具体类型必须是实现了该接口。
常见的场景是限定类型必须实现Comparable接口,我们来看代码:
java
public static <T extends Comparable> T max(T[] arr){
T max = arr[0];
for(int i=1; i<arr.length; i++){
if(arr[i].compareTo(max)>0){
max = arr[i];
}
}
return max;
}
max方法计算一个泛型数组中的最大值。计算最大值需要进行元素之间的比较,要求元素实现Comparable接口,所以给类型参数设置了一个上边界Comparable, T必须实现Comparable接口。
不过,直接这么编写代码,Java中会给一个警告信息,因为Comparable是一个泛型接口,它也需要一个类型参数,所以完整的方法声明应该是:
java
public static <T extends Comparable<T>> T max(T[] arr){
//主体代码
}
<T extends Comparable>是一种令人费解的语法形式,这种形式称为递归类型限制,可以这么解读:
- T表示一种数据类型,必须实现Comparable接口,
- 且必须可以与相同类型的元素进行比较。因为Comparable接口中的类型参数也是T,(当然可以指定为其他类型,语法上没问题,不过意义不大)
3. 上界为其他类型参数
Java支持一个类型参数以另一个类型参数作为上界。
比如下面一个容器类,它的泛型参数是E,类中定义了一些基本的方法。
java
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if(oldCapacity >= minCapacity){
return;
}
int newCapacity = oldCapacity * 2;
if(newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
public E get(int index) {
return (E)elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
}
现在有这样一个需求:给上面的DynamicArray类增加一个实例方法addAll,这个方法将参数容器中的所有元素都添加到当前容器里来
直觉上我们可以直接像下面这样写:
java
public void addAll(DynamicArray<E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
但是直接这么写有一些局限性,我们看使用它的代码:
java
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints); //会提示编译错误
numbers是一个Number类型的容器,ints是一个Integer类型的容器,我们希望将ints添加到numbers中,因为Integer是Number的子类,应该说,这是一个合理的需求和操作。但Java会在numbers.addAll(ints)这行代码上提示编译错误:addAll需要的参数类型为DynamicArray,而传递过来的参数类型为DynamicArray,不适用。Integer是Number的子类,怎么会不适用呢?事实就是这样,确实不适用,而且是很有道理的,假设适用,我们看下会发生什么。
java
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints; //假设这行是合法的
numbers.add(new Double(12.34));
那最后一行就是合法的,这时,DynamicArray中就会出现Double类型的值,而这显然破坏了Java泛型关于类型安全的保证。
通过这次的分析我们可以进一步理解泛型对类型安全性的保证 :泛型之间的相互赋值,类型必须完全一致。A = B; 把B赋给A,如果A的泛型类型是T,那么B的泛型类型也必须是T,不能说B的泛型类型是T的子类,否则泛型对类型安全性的保证就没有意义了。
不过,我们想将Integer添加到Number容器中的需求是合理的,并没有什么问题,这个问题可以通过类型限定来解决:
java
public <T extends E> void addAll(DynamicArray<T> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
E是DynamicArray的类型参数,T是addAll的类型参数,T的上界限定为E,这样,下面的代码就没有问题了:
java
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
4. 使用<? extends E> 代替 < T extends E>
为了将Integer对象添加到Number容器中,我们的类型参数使用了其他类型参数作为上界,我们提到,这种写法有点烦琐,它可以替换为更为简洁的通配符形式:
java
public void addAll(DynamicArray<? extends E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
这个方法没有定义类型参数 ,c的类型是DynamicArray<? extends E>, ?表示通配符 ,<? extends E>表示有限定通配符,匹配E或E的某个子类型 ,具体什么子类型是未知的。使用这个方法的代码不需要做任何改动,还可以是:
java
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
那么问题来了,同样是extends关键字,同样应用于泛型,和<?extends E>到底有什么关系?它们用的地方不一样,我们解释一下:
1)用于定义类型参数 ,它声明了一个类型参数T,可放在 泛型类 定义中类名后面 、泛型 方法返回值前面。
因为在类中定义了一个类型参数T,所以就可以在类中使用这个T类型。
因为在方法上定义了一个类型参数T,所以就可以在方法中的参数,方法内、方法的返回值使用这个T类型 。 细细体会。
2)<? extends E>用于实例化类型参数 ,它用于实例化 泛型 变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。 比如上面的 public void addAll(DynamicArray<? extends E> c) 我定义了一个参数变量c,它的类型是DynamicArray,由于DynamicArray是泛型类,所以我需要传入具体的泛型,是我传入了<? extends E> 它表示的是类型是E或E的某个子类型。
细细体会 <? extends E>用于实例化类型参数,并没有定义一个类型。
<? super E>的使用
还有一种通配符,与形式<? extends E>正好相反,它的形式为<? super E>,称为超类型通配符,表示E的某个父类型。它有什么用呢?有了它,我们就可以更灵活地写入了。
如果没有这种语法,写入会有一些限制。来看个例子,我们给DynamicArray添加一个方法:
java
public void copyTo(DynamicArray<E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}
这个方法也很简单,将当前容器中的元素添加到传入的目标容器中。我们可能希望这么使用:
java
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers); // 编译器报错
Integer是Number的子类,将Integer对象拷贝入Number容器,这种用法应该是合情合理的,但Java会提示编译错误,理由我们之前也说过了,期望的参数类型是Dynamic-Array, DynamicArray并不适用。如之前所说,一般而言,不能将DynamicArray看作DynamicArray,但我们这里的用法是没有问题的,Java解决这个问题的方法就是超类型通配符,可以将copyTo代码改为:
java
public void copyTo(DynamicArray<? super E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}
问:在前面的< T extends E>讲解中,我们知道泛型擦除之后会统一转换为E类型 ,那< ? super E>擦除会转换成什么类型呢?
当然也是E类型。细细体会< T extends E>实现灵活读 和< ? super extends E>实现灵活写
测试作业:在当前类中有这样一个方法,它要操作的数据类型是E,然后它需要一个参数arg。
场景1:进行一系列逻辑操作之后我需要把参数arg赋值给类型为E的数据--> E = arg ,那参数arg的泛型参数该如何设计? 场景2:进行一些列操作逻辑之后,需要把E类型的数据赋值给agr--> agr = E,那参数arg的泛型参数如何设计?
<?>的使用
< ? >,称为无限定通配符。我们来看个例子,在DynamicArray中查找指定元素,代码如下:
java
public static int indexOf(DynamicArray<? > arr, Object elm){
for(int i=0; i<arr.size(); i++){
if(arr.get(i).equals(elm)){
return i;
}
}
return -1;
}
其实,这种无限定通配符形式也可以改为使用类型参数。
java
public static <T> int indexOf(DynamicArray<T> arr, Object elm)
不过,通配符形式更为简洁。虽然通配符形式更为简洁,但是只能读不能写。比如交换两个元素的位置,看如下代码:
java
public static void swap(DynamicArray<? > arr, int i, int j){
Object tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
代码看上去应该是正确的,但Java会提示编译错误,两行set语句都是非法的。存在写操作。
不过,借助带类型参数的泛型方法,这个问题可以如下解决:
java
// 当然可以写操作 因为类型一致 都是T类型
private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){
T tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
public static void swap(DynamicArray<? > arr, int i, int j){
swapInternal(arr, i, j);
}
swap可以调用swapInternal,而带类型参数的swapInternal可以写入。Java容器类中就有类似这样的用法,公共的 API 是通配符形式,形式更简单,但内部调用带类型参数的方法。
除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数,比如,将src容器中的内容复制到dest中:
java
public static <D, S extends D> void copy(DynamicArray<D> dest,
DynamicArray<S> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
S和D有依赖关系,要么相同,要么S是D的子类,否则类型不兼容,有编译错误。不过,上面的声明可以使用通配符简化,两个参数可以简化为一个,如下所示:
java
public static <D> void copy(DynamicArray<D> dest,
DynamicArray<? extends D> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
泛型的局限性和细节
一项技术,往往只有理解了其局限性,才算是真正理解了它,才能更好地应用它
泛型中的一些细节和局限性,这些局限性主要与Java的实现机制有关。Java中,泛型是通过类型擦除来实现的,类型参数在编译时会被替换为Object或者指定的上界或者下界,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性
下面将从以下几个方面来介绍这些细节和局限性:
❑ 使用泛型类、方法和接口。
❑ 定义泛型类、方法和接口。
❑ 泛型与数组。
使用泛型类、方法和接口
1. 基本类型不能用于实例化类型参数
因为类型参数会被替换为Object,所以Java泛型中不能使用基本数据类型,也就是说,类似下面的写法是不合法的:
Pair<int> minmax = new Pair<int>(1,100);
解决方法是使用基本类型对应的包装类。
2. 运行时类型信息不适用于泛型。
在Java程序运行期间,每个类在内存中都会有一份类型信息,而每个对象也都保存着其对应类型信息的引用,这个类型信息Java中通过Class对象来保存。
获取类型信息可以通过 类名.class 或者 对象.getClass()。需要注意的是:这个类型对象只有一份,与泛型无关,所以Java不支持类似如下写法:
Pair<Integer>.class
因为类型对象只有一份,与泛型无关,所以一个泛型对象的getClass方法的返回值与原始类型对象是相同的,不管这个泛型对象的泛型是什么。比如,下面代码的输出都是true:
java
Pair<Integer> p1 = new Pair<Integer>(1,100);
Pair<String> p2 = new Pair<String>("hello", "world");
System.out.println(Pair.class==p1.getClass()); //true
System.out.println(Pair.class==p2.getClass()); //true
我们知道instanceof关键字可以用于判断对象的类型,而instanceof是运行时判断,也与泛型无关,所以,Java也不支持类似如下写法:
if(p1 instanceof Pair<Integer>)
不过,Java支持如下写法:
if(p1 instanceof Pair<? >)
3. 加深对Java中泛型擦除的理解
请问在一个类中定义如下重载方法是否正确呢?
java
public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)
好像上面的写法是可以的,因为虽然参数都是DynamicArray,但实例化类型不同,一个是DynamicArray,另一个是DynamicArray,同样,遗憾的是,Java不允许这种写法,理由同样是类型擦除后它们的声明是一样的。
定义泛型类、方法和接口
1. 无法通过类型参数来创建对象
比如,T是类型参数,下面的写法都是非法的:
java
T elm = new T();
T[] arr = new T[10];
为什么非法呢?因为如果允许,那么用户会以为创建的就是对应类型的对象 ,但由于类型擦除,Java只能创建Object类型的对象 ,而无法创建T类型的对象,容易引起误解,所以Java干脆禁止这么做。
2. 使用泛型声明的类型参数不能在静态变量和静态方法中使用
对于泛型类声明的类型参数,可以在实例变量和方法中使用,但在静态变量和静态方法中是不能使用的。类似下面这种写法是非法的
java
public class Singleton<T> {
private static T instance;
public synchronized static T getInstance(){
if(instance==null){
//创建实例
}
return instance;
}
}
如果合法,那么对于每种实例化类型,都需要有一个对应的静态变量和方法。但由于类型擦除,Singleton类型只有一份,静态变量和方法都是类型的属性,且与类型参数无关,所以不能使用泛型类类型参数
不能创建泛型数组
不能创建泛型数组其实很好理解,原因还是因为操作不当会引起类型转换异常。
到这里我们对Java中泛型的限制和细节应该有了一个全局的认识了:不管是什么限制都是围绕2点,第一 运行期间泛型不存在已被擦除。第二 Java必须保证使用泛型后只要没有在编译期间报错那在运行期间就不会发生类型转换异常。
如果Java允许创建泛型数组,则会发生非常严重的问题,我们看看具体会发生什么:
java
Pair<Object, Integer>[] options = new Pair<Object, Integer>[3];
Object[] objs = options;
objs[0] = new Pair<Double, String>(12.34, "hello");
如果可以创建泛型数组options,那它就可以赋值给其他类型的数组objs,而最后一行明显错误的赋值操作,则既不会引起编译错误,也不会触发运行时异常,因为Pair<Double, String>的运行时类型是Pair,和objs的运行时类型Pair[]是匹配的。但我们知道,它的实际类型是不匹配的,在程序的其他地方,当把objs[0]作为Pair<Object, Integer>进行处理的时候,一定会触发异常。
但现实需要能够存放泛型对象的容器,怎么办呢?
- 使用原始类型数组:
java
Pair[] options = new Pair[]{
new Pair<String, Integer>("1元",7),
new Pair<String, Integer>("2元", 2),
new Pair<String, Integer>("10元", 1)};
- 使用泛型容器
对于泛型容器,jdk中就提供了很多,其实内部还是使用Object[]数组来存放数据,然后再通过反射来完成一些方法。
总结
至此,关于Java中的泛型,我就知道这么些,如果可以很好的理解泛型,有助于我们设计简洁、灵活、稳健的代码,对阅读其它优秀的源码也是有帮助的。如果有错误,欢迎指正。