【Generic】详解 Java 中的泛型(上)

本是古典 何须时尚 2023-10-03 13:08 130阅读 0赞

1. 未使用泛型———通过继承设计通用程序

在 Java 增加泛型类型之前,通用程序的设计就是利用 继承 实现的:你可以将方法的参数类型设置为基类,那么该方法就可以接受这个基类中的任何子类作为参数,这样的方法将会更具有通用性。

如:自定义一个静态内部类,此类中维护一个 Object 类型的数组,然后,通过调用其 add(Object) 方法,往其数组中添加元素。代码如下:

  1. public class GenericTest {
  2. static class MyList {
  3. private Object[] elements = new Object[0];
  4. public Object get(int index) {
  5. return elements[index];
  6. }
  7. public void add(Object o) {
  8. int length = elements.length;
  9. Object[] newElements = new Object[length + 1];
  10. for(int i = 0; i < length; i++) {
  11. newElements[i] = elements[i];
  12. }
  13. newElements[length] = o;
  14. elements = newElements;
  15. }
  16. }
  17. }

由于 add(Object) 方法的形参类型是 Object类型,所以,在实际调用时可以传入任意类型。这里,我以 IntegerString 类型为例:

  1. public static void main(String[] args) {
  2. MyList myList = new MyList();
  3. myList.add(1);
  4. myList.add("hello");
  5. Integer i = (Integer)myList.get(0);
  6. String s = (String)myList.get(1);
  7. System.out.println(i);
  8. System.out.println(s);
  9. }

上述代码:往数组中添加了两个元素,类型分别为:Integer、String。然后,获取这两个元素时,分别对其返回类型进行了强制转换。由于我是知道每个元素的真实的类型,在强制转换时,类型与之对应。所以,将 Object 类型转换为 Integer、String 类型时没有报错,所以,上述程序可以正常运行。

但是,在项目开发中,你的数组不可能只存储这几个元素。这时,强制转换时,必须得考虑到每个元素的真实类型。如果没有考虑到,会发生什么样的后果?

接下来我们来试试。假设我们不知道数组的第一个元素类型,并认为它是 String 类型(将 Integer 修改成 String)。如下:

  1. public static void main(String[] args) {
  2. MyList myList = new MyList();
  3. myList.add(1);
  4. myList.add("hello");
  5. String i = (String)myList.get(0);
  6. String s = (String)myList.get(1);
  7. System.out.println(i);
  8. System.out.println(s);
  9. }

运行后,报了类转换异常。

Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.tiandy.zzc.generic.GenericTest.main(GenericTest.java:39)

这显然符合我们预料的,我们添加的第一个元素是 1,明显是一个 int 类型,将它强制转换为 String,肯定会报错的。

总结一下上述代码的问题:

  1. 当我们获取一个值的时候,必须进行强制类型转换(有可能会出现类转换异常)
  2. 当我们插入一个值的时候,无法约束预期的类型。当我们使用数据的时候,需要将获取的 Object 对象转换为我们期望的类型,如果向集合中添加了非预期的类型,编译时我们不会收到任何的错误提示,但当我们运行程序时却会报 ClassCastException 异常

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。

针对上述的问题,泛型提供了更好的解决方案:类型参数

2. 泛型

类型参数的命名风格:推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。

一般使用 T 代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数,我们可能使用字母表中 T 的临近的字母,比如 S

使用泛型改造上述代码:

  1. public class GenericTestTwo {
  2. static class MyList<T> {
  3. private Object[] elements = new Object[0];
  4. public T get(int index) {
  5. return (T)elements[index];
  6. }
  7. public void add(T o) {
  8. int length = elements.length;
  9. Object[] newElements = new Object[length + 1];
  10. for(int i = 0; i < length; i++) {
  11. newElements[i] = elements[i];
  12. }
  13. newElements[length] = o;
  14. elements = newElements;
  15. }
  16. }
  17. }

MyList 类用一个类型参数来指出元素的类型:

  1. public static void main(String[] args) {
  2. MyList<String> myList = new MyList();
  3. myList.add("hello");
  4. // 编译报错
  5. //myList.add(1);
  6. String s = myList.get(0);
  7. System.out.println(s);
  8. }

这样的代码具有更好的可读性,我们一看就知道该集合用来保存 String 类型的对象

现在,如果我们向 MyList<String>添加 Integer 类型的对象,将会出现编译错误。编译器会自动帮我们检查,避免向集合中插入错误类型的对象,从而使得程序具有更好的安全性.

3. 泛型的实现原理 ——— 泛型擦除

什么叫泛型擦除?

Java 中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。即:泛型信息不会进入到运行时阶段。

咱们来看看下面这个例子:

  1. public class GenericTestTwo {
  2. static class MyList<T> {
  3. private Object[] elements = new Object[0];
  4. ...
  5. }
  6. public static void main(String[] args) {
  7. MyList<String> stringMyList = new MyList();
  8. MyList<Integer> integerMyList = new MyList<>();
  9. // true
  10. System.out.println(stringMyList.getClass() == integerMyList.getClass());
  11. }
  12. }

一个是 MyList<String> 泛型类型,一个是 MyList<Integer>泛型类型,然后,获取各自对象的类信息(getClass() 方法)进行比较,最后发现结果为 true。因为,在编译期间,所有的泛型信息都会被擦除,MyList<Integer>MyList<String> 类型,在编译后都会变成 List 类型(原始类型)。

Java 中的泛型基本上都是在编译器这个层次来实现的,这也是 Java 的泛型被称为“伪泛型”的原因。

对上述代码进行反编译:

  1. public class GenericTestTwo
  2. {
  3. static class MyList
  4. {
  5. ...
  6. }
  7. public static void main(String args[])
  8. {
  9. MyList stringMyList = new MyList();
  10. MyList integerMyList = new MyList();
  11. System.out.println(stringMyList.getClass() == integerMyList.getClass());
  12. }
  13. }

总结:泛型类型在逻辑上看成是多个不同的类型,实际上都是相同的类型。

原始类型
原始类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。无论何时定义一个泛型类型,相应的原始类型都会被自动提供。

原始类型的名字就是删去类型参数后的泛型类型的类名。擦除类型变量,并替换为 限定类型(T为无限定的类型变量,用Object替换)。如下:

定义一个泛型:

  1. class Pair<T> {
  2. private T value;
  3. public T getValue() {
  4. return value;
  5. }
  6. public void setValue(T value) {
  7. this.value = value;
  8. }
  9. }

对应的原始类型:

  1. class Pair {
  2. private Object value;
  3. public Object getValue() {
  4. return value;
  5. }
  6. public void setValue(Object value) {
  7. this.value = value;
  8. }
  9. }

因为在 Pair<T>中,T是一个无限定的类型变量,所以用 Object 替换。如果是 Pair<T extends Number>,擦除后,类型变量用 Number 类型替换。

4. 突破泛型约束

上面有讲述到,泛型约束只会存在编译期,在运行期时会进行泛型擦除(泛型约束不生效了),那么,真的会这样吗?看看下面这个例子吧:

  1. public class GenericTestTwo {
  2. static class MyList<T> {
  3. private Object[] elements = new Object[0];
  4. public T get(int index) {
  5. return (T)elements[index];
  6. }
  7. public void add(T o) {
  8. int length = elements.length;
  9. Object[] newElements = new Object[length + 1];
  10. for(int i = 0; i < length; i++) {
  11. newElements[i] = elements[i];
  12. }
  13. newElements[length] = o;
  14. elements = newElements;
  15. }
  16. }
  17. public static void main(String[] args) throws Exception{
  18. MyList<Integer> integerMyList = new MyList<>();
  19. integerMyList.add(1);
  20. // 通过反射,调用 MyList#add() 方法
  21. Class<? extends MyList> aClass = integerMyList.getClass();
  22. Method method = aClass.getMethod("add", Object.class);
  23. method.invoke(integerMyList, "zzc");
  24. // zzc
  25. System.out.println(integerMyList.get(1));
  26. }
  27. }

在上面我们定义了 MyList<Integer> 泛型类型,它只能存储 Integer 类型,如果存储其它类型,如 String 类型,通过显示调用 add() 方法(integerMyList.add(“java”);),编译器肯定会报错。因为它进行了类型约束。

但在运行时,泛型信息已被擦除,类型变量已转换为了 Object 类型,通过反射,就可以通过 MyList#add() 方法添加其它类型的数据。

但是,并不推荐以这种方式操作泛型类型,因为这违背了泛型的初衷(减少强制类型转换以及确保类型安全)

不知大家有没有这个疑问:

既然类型变量会在编译的时候擦除变为原始类型 Object,既然类型擦除了,那又如何保证我们只能使用泛型变量限定的类型呢?对于这个问题,java 是如何解决这个问题的呢?

java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。

如下代码:

  1. public static void main(String[] args) {
  2. Pair<Integer> pair=new Pair<Integer> ();
  3. pair.setValue(3);
  4. Integer integer=pair.getValue();
  5. System.out.println(integer);
  6. }

编译器质性步骤:

  1. 擦除 getValue() 方法的返回类型后将返回 Object 类型
  2. 编译器自动插入 Integer 的强制类型转换

发表评论

表情:
评论列表 (有 0 条评论,130人围观)

还没有评论,来说两句吧...

相关阅读

    相关 (Generic) 优点

    泛型引入前编程的痛点 `JDK 1.5` 版本以前没有泛型,使用 Object 来实现不同类型的处理,有两个缺点 1、每次使用时都需要`强制转换`; 2、在`编译