初探Java类型擦除

简介: 本篇博客主要介绍了Java类型擦除的定义,详细的介绍了类型擦除在Java中所出现的场景。 1. 什么是类型擦除 为了让你们快速的对类型擦除有一个印象,首先举一个很简单也很经典的例子。 // 指定泛型为String List list1 = new ArrayList(); // 指定泛型为Integer List list2 = new ArrayList(); System.out.println(list1.getClass() == list2.getClass()); // true 上面的判断结果是true。

本篇博客主要介绍了Java类型擦除的定义,详细的介绍了类型擦除在Java中所出现的场景。

1. 什么是类型擦除

为了让你们快速的对类型擦除有一个印象,首先举一个很简单也很经典的例子。

// 指定泛型为String
List<String> list1 = new ArrayList<>();
// 指定泛型为Integer
List<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass()); // true

上面的判断结果是true。代表了两个传入了不同泛型的List最终都编译成了ArrayList,成为了同一种类型,原来的泛型参数String和Integer被擦除掉了。这就是类型擦除的一个典型的例子。

而如果我们说到类型擦除为什么会出现,我们就必须要了解泛型。

2. 泛型

2.1. 泛型的定义

随着2004年9月30日,工程代号为Tiger的JDK 1.5发布,泛型从此与大家见面。JDK 1.5在Java语法的易用性上作出了非常大的改进。除了泛型,同版本加入的还有自动装箱、动态注解、枚举、可变长参数、foreach循环等等。

而在1.5之前的版本中,为了让Java的类具有通用性,参数类型和返回类型通常都设置为Object,可见,如果需要不用的类型,就需要在相应的地方,对其进行强制转换,程序才可以正常运行,十分麻烦,稍不注意就会出错。

泛型的本质就是参数化类型。也就是,将一个数据类型指定为参数。引入泛型有什么好处呢?

泛型可以将JDK 1.5之前在运行时才能发现的错误,提前到编译期。也就是说,泛型提供了编译时类型安全的检测机制。例如,一个变量本来是Integer类型,我们在代码中设置成了String,没有使用泛型的时候只有在代码运行到这了,才会报错。

而引入泛型之后就不会出现这个问题。这是因为通过泛型可以知道该参数的规定类型,然后在编译时,判断其类型是否符合规定类型。

泛型总共有三种使用方法,分别使用于类、方法和接口。

3. 泛型的使用方法

3.1 泛型类

3.1.1 定义泛型类

简单的泛型类可以定义为如下。

public class Generic<T> {
    T data;
    
    public Generic(T data) {
        setData(data);
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}

其中的T代表参数类型,代表任何类型。当然,并不是一定要写成T,这只是大家约定俗成的习惯而已。有了上述的泛型类之后我们就可以像如下的方式使用了。

3.1.2 使用泛型类

// 假设有这样一个具体的类
public class Hello {
    private Integer id;

    private String name;

    private Integer age;

    private String email;
}

// 使用泛型类
Hello hello = new Hello();
Generic<Hello> result = new Generic<>();
resule.setData(hello);

// 通过泛型类获取数据
Hello data = result.getData();

当然如果泛型类不传入指定的类型的话,泛型类中的方法或者成员变量定义的类型可以为任意类型,如果打印result.getClass()的话,会得到Generic

3.2. 泛型方法

3.2.1 定义泛型方法

首先我们看一下不带返回值的泛型方法,可以定义为如下结构。

// 定义不带返回值的泛型方法
public <T> void genericMethod(T field) {
    System.out.println(field.getClass().toString());
}

// 定义带返回值的泛型方法
private <T> T genericWithReturnMethod(T field) {
    System.out.println(field.getClass().toString());
    return field;
}

3.2.2 调用泛型方法

// 调用不带返回值泛型方法
genericMethod("This is string"); // class java.lang.String
genericMethod(56L); // class java.lang.Long

// 调用带返回值的泛型方法
String test = genericWithReturnMethod("TEST"); // TEST class java.lang.String

带返回值的方法中,T就是当前函数的返回类型。

3.3. 泛型接口

泛型接口定义如下

public interface genericInterface<T> {
}

使用的方法与泛型类类似,这里就不再赘述。

4. 泛型通配符

什么是泛型通配符?官方一点的解释是

Type of unknown.

也就是无限定的通配符,可以代表任意类型。用法也有三种,<?>,<? extends T>和<? super T>。

既然已经有了T这样的代表任意类型的通配符,为什么还需要这样一个无限定的通配符呢?是因为其主要解决的问题是泛型继承带来的问题。

4.1. 泛型的继承问题

首先来看一个例子

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList;

我们知道,Integer是继承自Number类的。

public final class Integer extends Number implements Comparable {
    ....

}

那么上述的代码能够通过编译吗?肯定是不行的。Integer继承自Number不代表List 和 List之间有继承关系。那通配符的应用场景是什么呢?

4.2. 通配符的应用场景

在其他函数中,例如JavaScript中,一个函数的参数可以是任意的类型,而不需要进行任意的类型转换,所以这样的函数在某些应用场景下,就会具有很强的通用性。

而在Java这种强类型语言中,一个函数的参数类型是固定不变的。那如果想要在Java中实现类似于JavaScript那样的通用函数该怎么办呢?这也就是为什么我们需要泛型的通配符。

假设我们有很多动物的类, 例如Dog, Pig和Cat三个类,我们需要有一个通用的函数来计算动物列表中的所有动物的腿的总数,如果在Java中,要怎么做呢?

可能会有人说,用泛型啊,泛型不就是解决这个问题的吗?泛型必须指定一个特定的类型。正式因为泛型解决不了...才提出了泛型的通配符。

4.3. 无界通配符

无界通配符就是?。看到这你可能会问,这不是跟T一样吗?为啥还要搞个?。他们主要区别在于,T主要用于声明一个泛型类或者方法,?主要用于使用泛型类和泛型方法。下面举个简单的例子。

// 定义打印任何类型列表的函数
public static void printList(List<?> list) {
    for (Object elem: list) {
        System.out.print(elem + " ");
    }
}

// 调用上述函数
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> stringList = Arrays.asList("one", "two", "three");
printList(li);// 1 2 3 
printList(ls);// one two three

上述函数的目的是打印任何类型的列表。可以看到在函数内部,并没有关心List中的泛型到底是什么类型的,你可以将<?>理解为只提供了一个只读的功能,它去除了增加具体元素的能力,只保留与具体类型无关的功能。从上述的例子可以看出,它只关心元素的数量以及其是否为空,除此之外不关心任何事。

再反观T,上面我们也列举了如何定义泛型的方法以及如果调用泛型方法。泛型方法内部是要去关心具体类型的,而不仅仅是数量和不为空这么简单。

4.4. 上界通配符<? extends T>

既然?可以代表任何类型,那么extends又是干嘛的呢?

假设有这样一个需求,我们只允许某一些特定的类型可以调用我们的函数(例如,所有的Animal类以及其派生类),但是目前使用?,所有的类型都可以调用函数,无法满足我们的需求。

private int countLength(List< ? extends Animal> list) {...}

使用了上界通配符来完成这个公共函数之后,就可以使用如下的方式来调用它了。

List<Pig> pigs = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
List<Cat> cats = new ArrayList<>();

// 假装写入了数据
int sum = 0;
sum += countLength(pigs);
sum += countLength(dogs);
sum += countLength(cats);

看完了例子,我们就可以简单的得出一个结论。上界通配符就是一个可以处理任何特定类型以及是该特定类型的派生类的通配符。

可能会有人看的有点懵逼,我结合上面的例子,再简单的用人话解释一下:上界通配符就是一个啥动物都能放的盒子。

4.5. 下界通配符<? super Animal>

上面我们聊了上界通配符,它将未知的类型限制为特定类型或者该特定的类型的子类型(也就是上面讨论过的动物以及一切动物的子类)。而下界通配符则将未知的类型限制为特定类型或者该特定的类型的超类型,也就是超类或者基类。

在上述的上界通配符中,我们举了一个例子。写了一个可以处理任何动物类以及是动物类的派生类的函数。而现在我们要写一个函数,用来处理任何是Integer以及是Integer的超类的函数。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

5. 类型擦除

简单的了解了泛型的几种简单的使用方法之后,我们回到本篇博客的主题上来——类型擦除。泛型虽然有上述所列出的一些好处,但是泛型的生命周期只限于编译阶段。

本文最开始的给出的样例就是一个典型的例子。在经过编译之后会采取去泛型化的措施,编译的过程中,在检测了泛型的结果之后会将泛型的相关信息进行擦除操作。就像文章最开始提到的例子一样,我们使用上面定义好的Generic泛型类来举个简单的例子。

Generic<String> generic = new Generic<>("Hello");
Field[] fs = generic.getClass().getDeclaredFields();
for (Field f : fs) {
    System.out.println("type: " + f.getType().getName()); // type: java.lang.Object
}

getDeclaredFields是反射中的方法,可以获取当前类已经声明的各种字段,包括public,protected以及private。

可以看到我们传入的泛型String已经被擦除了,取而代之的是Object。那之前的String和Integer的泛型信息去哪儿了呢?可能这个时候你会灵光一闪,那是不是所有的泛型在被擦除之后都会变成Object呢?别着急,继续往下看。

当我们在泛型上面使用了上界通配符以后,会有什么情况发生呢?我们将Generic类改成如下形式。

public class Generic<T extends String> {
    T data;

    public Generic(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

然后再次使用反射来查看泛型擦除之后类型。这次控制台会输出type: java.lang.String。可以看到,如果我们给泛型类制定了上限,泛型擦除之后就会被替换成类型的上限。而如果没有指定,就会统一的被替换成Object。相应的,泛型类中定义的方法的类型也是如此。

6. 写在最后

如果各位发现文章中有问题的,欢迎大家不吝赐教,我会及时的更正。

参考:

  1. Java语言类型擦除
  2. 下界通配符
  3. List<?>和List的区别

相关:

相关文章
|
21天前
|
存储 Java 开发者
Java 中 Set 类型的使用方法
【10月更文挑战第30天】Java中的`Set`类型提供了丰富的操作方法来处理不重复的元素集合,开发者可以根据具体的需求选择合适的`Set`实现类,并灵活运用各种方法来实现对集合的操作和处理。
|
21天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
41 2
|
22天前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
泛型擦除是指Java编译器在编译期间会移除所有泛型信息,使所有泛型类型在运行时都变为原始类型。例如,`List&lt;String&gt;` 和 `List&lt;Integer&gt;` 在JVM中都视为 `List`。因此,通过 `getClass()` 比较两个不同泛型类型的 `ArrayList` 实例会返回 `true`。此外,通过反射调用 `add` 方法可以向 `ArrayList&lt;Integer&gt;` 中添加字符串,进一步证明了泛型信息在运行时被擦除。
36 2
|
2月前
|
Java 编译器
Java“返回类型为 void 的方法不能返回一个值”解决
在 Java 中,如果一个方法的返回类型被声明为 void,那么该方法不应该包含返回值的语句。如果尝试从这样的方法中返回一个值,编译器将报错。解决办法是移除返回值语句或更改方法的返回类型。
|
2月前
|
设计模式 Java
Java“不能转换的类型”解决
在Java编程中,“不能转换的类型”错误通常出现在尝试将一个对象强制转换为不兼容的类型时。解决此问题的方法包括确保类型间存在继承关系、使用泛型或适当的设计模式来避免不安全的类型转换。
|
2月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
2月前
|
安全 Java
Java“不兼容类型” 错误怎么查找解决
在 Java 中遇到“不兼容类型”错误时,首先理解错误信息,它表明试图将一种类型赋给不兼容的类型。检查代码中类型不匹配的赋值、方法调用参数类型不匹配、表达式类型不兼容及泛型类型不匹配等问题。解决方法包括进行类型转换、修改代码逻辑、检查方法参数和返回类型以及处理泛型类型不匹配。通过这些步骤,可以有效解决“不兼容类型”错误,确保代码类型兼容性良好。
|
3月前
|
存储 Java Windows
java基础(9)数据类型中的char类型以及涉及到的转义字符
Java中的char类型可以存储一个中文字符,因为它占用两个字节。转义字符允许在代码中使用特殊字符,例如`\n`表示换行,`\t`表示制表符,`\\`表示反斜杠,`\'`表示单引号,`\"`表示双引号。可以使用`\u`后跟Unicode编码来表示特定的字符。
60 2
java基础(9)数据类型中的char类型以及涉及到的转义字符
|
2月前
|
Java 程序员 编译器
Java中的异常类型
Java中的异常类型
23 3
|
2月前
|
Java 开发者
Java“类 Y 中的方法 X 不能应用于给定类型”解决
在Java中遇到“类Y中的方法X无法应用于给定类型”的错误时,通常是因为方法调用时的参数类型与定义不符。解决此问题需检查方法签名,确保传递的参数类型正确无误,或使用显式类型转换以匹配方法所需的参数类型。这种错误提示帮助开发者及时修正类型不匹配的问题。