目录
泛型相关知识点
1.泛型的基本用法
泛型类
泛型接口
泛型方法
2.类型擦除
补充:无限定通配符---> ?
泛型的一个基本限制和其背后的实现机制
自动装箱
自动拆箱
泛型相关知识点
在java中 泛型是一个强大的特性 它允许在类、接口和方法中指定参数的类型 这样 类、接口和方法就可以在不指定具体类类型的情况下进行编写 直到在实例化时才指定具体的类型 这带来了几个重要的好处 包括类型安全、代码复用和清晰的代码结构
1.泛型的基本用法
泛型类
泛型类是在类定义时通过参数的类型 指定类中某些属性的类型
例如 一个简单的泛型类Box可能看起来像这样:
在这个例子中 T是一个类型参数(为了便于自己理解 我认为是参数的类型) 它可以在创建Box对象时指定
泛型接口
泛型接口与泛型类类似 但在接口中定义
例如:
泛型方法
泛型方法允许你在方法级别(返回值 参数类型....)上指定 参数的类型
例如:
2.类型擦除
Java的泛型是通过类型擦除来实现的 这意味着泛型信息只存在于编译时 而在运行时 Java虚拟机并不保留泛型参数的类型信息 因此 泛型在运行时并不提供额外的类型安全检查或类型信息 但是 你可以通过泛型来编写类型安全的代码 并在编译时检查类型错误
实例:
尽管上面的Box类使用了泛型T,但当你查看编译后的字节码时,你会发现T的类型信息并不存在。这是因为Java编译器在编译时将泛型类型信息擦除了,JVM将Box<T>视为Box类型(除非你有显式的类型边界,比如Box<T extends SomeClass>,那么T会被替换成SomeClass)。但是,由于Java的自动类型转换和类型推断机制,你仍然可以在不丢失类型安全的情况下使用泛型。
然而,需要注意的是,printClassOfT方法能够打印出t的实际类型(Integer或String),这是因为它是在运行时通过t.getClass()获取的,而不是通过泛型类型参数获取的。这证明了尽管泛型类型信息在编译期间被擦除了,但对象本身仍然保留了自己的类型信息。
类型擦除还意味着你不能在运行时检查泛型类型参数,例如你不能编写一个方法来检查Box对象是否是一个Box<Integer>类型的实例,因为所有的Box对象在运行时都被视为Box的实例 而没有任何关于T的具体信息。这是Java泛型设计的一个限制
示例
以Java为例 如果你有一个List<String>的实例 在编译后 这个实例的类型信息(即<String>部分)会被擦除 运行时JVM只会将其视为List类型 这意味着 在运行时 我们无法判断这个列表是否是存储字符串的
总结一下,在运行时,JVM将List<String>视为List类型,但它不是Object类型。类型安全主要是通过编译时的类型检查和运行时对元素的实际处理(如类型转换)来维护的。泛型信息在编译后被擦除,但编译器生成的代码和运行时检查确保了类型安全
类型擦除的详细解释
类型擦除主要指的是在编译期间 将泛型信息从代码中擦除的过程
含义
编译时处理:泛型信息(如参数的类型)只在编译阶段存在 用于编译器进行类型检查
运行时无泛型信息:在Java虚拟机运行时 泛型信息被擦除 泛型类型被当作它们的原始类型或无限定通配符(?)类型来处理
在Java的泛型实现中 无论是使用无限定通配符?还是具体的类型参数(如String) 在编译后的字节码中都会被擦除为它们的原始类型或边界类型(如果有的话) 对于无限定通配符 由于它代表任意类型 因此在类型擦除后 其行为更像是Object类型 但实际上并没有明确声明为Object
补充:无限定通配符---> ?
使用无限定通配符 ? 的泛型类型(如List<?>)可以被视为持有任何类型的对象,这与持有Object类型对象的集合(如List<Object>)在类型兼容性上有很大的相似性。但是,它们之间有一个关键的区别:List<?>类型的集合不能添加除了null之外的任何元素,因为它是一个未知类型的列表;而List<Object>则可以添加任何类型的对象,因为所有类型都是Object的子类型
用途和限制:
无限定通配符?主要用于表示未知的类型 这在处理泛型集合时非常有用 尤其是当我们只需要从集合中读取元素而不需要添加元素时 使用?可以避免类型安全问题 因为它强制我们以类型安全的方式使用集合
相比之下 List<Object>明确声明了集合可以持有任何类型的对象 但它也允许你向集合中添加任何类型的对象 这可能会导致类型安全问题
反射:
关于反射 我们接下来会出一篇文章来讲解 敬请稍后
在使用反射时 List<?>和List<Object>之间的区别变得更加明显 由于List<?>在运行时被擦除为原始类型List 因此通过反射获取的元素类型信息将是Object(或者更准确地说 是擦除后类型的元素类型 但在这个情况下是Object) 但这不意味着List<?>就是List<Object> List<Object>在反射中会有更明确的类型信息 即Object
无限定通配符 ? 的使用案例1
在这个例子中,尽管我们知道list实际上是一个ArrayList<String>(但这只是为了说明,实际上编译器不知道),但我们不能向List<?>类型的list中添加任何除了null之外的元素。这是因为编译器在编译时无法验证这种添加操作是否类型安全。
因此,List<?>类型的集合被设计为只能读取元素(通过迭代器等),而不能添加除了null之外的任何元素。这种设计有助于防止类型错误,并确保集合的类型安全
案例2
在遍历List<?>类型的集合时 由于?代表未知类型 我们通常只能将元素作为Object类型来处理 如果我们需要将元素转换为特定类型 则应该使用instanceof检查来确保类型安全
但在大多数情况下 当我们使用List<?>时 我们应该只是读取元素而不进行修改
泛型的一个基本限制和其背后的实现机制
在Java中 泛型的实现机制是通过类型擦除来完成的 这一机制限制了 泛型只能用于对象类型(即引用类型) 而不能直接用于基本数据类型
下面详细解释这一机制及其背后的原因
泛型与类型安全
泛型是Java SE 5中引入的一个特性 它提供了一种编译时类型安全检测机制 允许程序员在编译时期就检查到非法的类型 例如 我们可以创建一个List<String>来确保这个列表只能包含字符串类型的元素 尝试向其中添加非字符串类型的元素会导致编译失败
类型擦除
然后 泛型信息在运行时是不可用的 这是因为Java的泛型是通过类型擦除来实现的 类型擦除意味着在编译时 编译器会将泛型代码中的类型参数替换为它们的界限类型(如果有的话 通常为Object) 这个过程会移除所有的泛型类型信息 例如 List<String>在运行时会被当作List来处理 JVM不知道这个列表原本是只包含字符串的
为什么不能用于基本数据类型
由于类型擦除的存在 泛型在运行时失去了其类型信息 而基本数据类型并不是对象 它们无法继承自Object 也没有null值的概念 如果Java允许泛型使用基本数据类型 那么在类型擦除后 编译器无法将泛型类型参数替换为有效的运行时类型 因为基本数据类型无法被当作对象来处理
解决方案:自动装箱和拆箱
为了解决这个问题 Java为每一个基本数据类型都提供了对应的包装类(如Integer包装int Double包装double等) 这些包装类都是对象 可以继承自Object 也可以拥有null值 因此 当需要使用泛型与基本数据类型一起工作时 可以使用它们的包装类来替代 这就是自动装箱和拆箱发挥作用的地方:自动装箱是将基本数据类型转换为对应的包装类对象 而自动拆箱则执行相反的操作
自动装箱
自动装箱是指将基本数据类型自动转换为对应的包装类对象 这个过程是由编译器自动完成的 无需显式调用包装类的构造方法
示例:
在上面的例子中 int类型的变量i被自动装箱成了Integer类型的对象integer 需要注意的是 自动装箱并不是简单地调用new Integer(i) 因为这样做每次都会创建一个新的对象 而自动装箱会尝试重用现有的对象(通过调用Integer.valueOf(int)方法) 这称为缓存或”池化”
对于Integer 范围在 -128到127之间的值会被缓存
自动拆箱
自动拆箱是指将包装类对象自动转换为对应的基本数据类型 这个过程同样是由编译器自动完成的 无需显式调用包装类的方法(如intValue()方法)
示例:
在上面的例子中 Integer类型的对象integer被自动拆箱成了int类型的变量i
注意事项
- 性能问题:虽然自动装箱和拆箱提供了方便 但它们可能会引入性能问题 因为每次装箱和拆箱操作都可能涉及到对象的创建和销毁 特别是当这些操作发生在循环或频繁调用的方法中时 性能问题可能会更加明显
- 空指针异常:自动拆箱可能导致NullPointerException 如果尝试拆箱一个null的包装类对象
- 缓存机制:对于Integer、Boolean、Byte、Character、Short和Long(对于Long和Integer 只有一定范围内的值会被缓存) Java提供了缓存机制以减少对象创建的开销 但是 对于其他包装类(比如Double和Float) 则没有这样的缓存机制
- 比较:使用包装类对象进行比较时 应该注意equals()方法和 == 操作符的区别
Equals()方法会比较两个对象的值是否相等 而==操作符会比较两个对象的引用是否相同
因此 在自动装箱的上下文中 使用==来比较两个Integer对象可能会得到意外的结果 特别是当它们位于缓存范围内时