1. 什么是泛型
1.1 泛型的来源
在 Java 编程思想中, 有这样一句对泛型介绍的话: 一般的类和方法, 只能使用具体的类型: 要么是基本类型, 要么是自定义的类. 如果要编写可以应用于多种类型的代码, 这种刻板的限制对代码束缚就会很大.
于是为了解决这样的一个问题, 在 JDK1.5 中就引入了一种新的语法, 那就是泛型. 泛型在代码层面上就是对类型实现了参数化.
1.2 引入泛型
问题引入1:
public class Test {
public static void func(int val) {
System.out.println(val);
}
public static void main(String[] args) {
func(1);
}
}
在如上的代码中,有一个 func 方法, 但是这个方法只能传递 int 类型的参数, 但是如果我有需求让 func 方法可以对多种参数都能起到一个处理的作用, 那么我则需要这个 func 方法能够接收多种参数.
在还没接触到泛型的时候, 其实是可以使用方法的重载来让一个方法接收多种不同类型的参数, 如下所示:
public class Test {
// 接收 int 类型的参数
public static void func(int val) {
System.out.println(val);
}
// 接收 String 类型的参数
public static void func(String val) {
System.out.println(val);
}
// 接收 double 类型的参数
public static void func(double val) {
System.out.println(val);
}
public static void main(String[] args) {
// 传递 int 类型的值
func(1);
// 传递 String 类型的值
func("Alex");
// 传递 double 类型的值
func(1.2);
}
}
这样子就能够使用方法的重载来让 func 可以接收多种类型的值了. 但是使用这种方法, 需要写出很多重复性代码, 甚至还有可能每一个重载方法都需要类型的强转, 在代码量上出现许多冗余的代码, 并且代码的可维护性低, 成本高(如果需要让 func 方法在后期加上另外的功能, 需要在每一个重载方法上都进行改变).
问题引入2:
实现一个类, 类中包含一个数组成员, 使得该数组能够存放任意类型的数据, 也可以根据类中的方法来返回数组中指定下标的值.
先来拆解一下问题, 怎么指定一个数组, 来让这个数组可以存放不同类型的值?
这里可以想到使用 Object 数组. 因为 Object 类是所有类的父类, 所有类都可以向上转型为 Object 类, 使得 Object 数组可以存放任意的数据
Object[] arr = {1, "Alex", 1.2};
上面这一行代码是不会报错的, 这样我们就解决了这个问题中其中一小个问题.
接下来, 整个问题的实现思路:
class MyArray {
Object[] arr = new Object[100];
public void setVal(int pos, Object val) {
arr[pos] = val;
}
public Object getVal(int pos) {
return arr[pos];
}
}
这样, 就可以使用这个类来存放数据了.
MyArray array = new MyArray();
array.setVal(0, 1);
array.setVal(1, "Alex");
array.setVal(2, 1.2);
但是如果使用一个变量来接收这个下标的值就需要进行强转了
int val1 = (int) array.getVal(1);
String val2 = (String) array.getVal(2);
double val3 = (double) array.getVal(3);
所以上述代码可以看出, 将元素存放进去的时候很方便, 但是要将它取出来的时候就变得麻烦. 这不仅降低了代码的开发效率, 也影响了代码本身的可维护性.
使用 Object 数组的缺点:
- 存放元素的时候, 任何的元素都可以存放进去.
- 取出元素的时候, 需要进行类型的强转.
这就引出了上面描述泛型的定义: 在代码层面上, 将类型参数化. 那么什么是类型的参数化呢? 从上述两个例子中可以看出来, 想要实现一个方法能够接收多种不同类型的参数进行处理, 那么就可以在传参的过程中将传递的参数的类型也传递过去, 可以形象化的表示为: 传递的参数的类型也可以是一个参数一起传递过去. 这就说明了上面所说的 <将类型参数化> 了.
2. 泛型的使用
泛型类的写法
根据上面的 MyArray 类, 改成一个具有泛型效果的类
static class MyArray<T> {
T[] arr = (T[]) new Object[100];
public void setVal(int pos, T val) {
arr[pos] = val;
}
public T getVal(int pos) {
return arr[pos];
}
}
这样, 这个类就是一个泛型类
其中, 是一个占位符, 代表着这个类是一个泛型类.
泛型类的使用
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
myArray1.setVal(0, 0);
myArray1.setVal(1, 1);
myArray1.setVal(2, 2);
int val1 = myArray1.getVal(0);
MyArray<String> myArray2 = new MyArray<>();
myArray2.setVal(0, "Alex");
myArray2.setVal(1, "Alice");
myArray2.setVal(2, "Mana");
String val2 = myArray2.getVal(0);
}
泛型存在的两个最大的意义:
- 存放元素的时候, 会进行类型的检查.
- 取出元素的时候, 会自动进行类型的转换. 不需要进行强转.
这都是在编译期间的一种机制 -> 擦除机制
3. 泛型的擦除机制
上面说到, 泛型是在编译期间所使用的一种语法, 在运行期间是没有泛型这个概念的, 可以来看一下上面的 MyArray 的字节码文件.
从上述图片可以看出, 编译之后的字节码文件中并没有 的影子, 全部都被擦除成了 Object. 这就是泛型的擦除机制. 编译生成的字节码在运行过程中并不包含泛型的信息.