什么是泛型

什么是泛型

泛型是Java SE 1.5 的新特性,《Java 核心技术》中对泛型的定义是:
“泛型” 意味着编写的代码可以被不同类型的对象所重用。
可见泛型的提出是为了编写重用性更好的代码。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 比如常见的集合类 LinkedList:
public class LinkedList< E> extends AbstractSequentialList< E> implements
List< E>, Deque< E>, Queue< E>, Cloneable, Serializable {
//…
transient Link< E> voidLink;
//…
}
可以看到,LinkedList< E> 类名及其实现的接口名后有个特殊的部分 “< E>”,而且它的成员的类型 Link< E> 也包含一个 “< E>”,这个符号的就是 类型参数,它使得在运行中,创建一个 LinkedList 时可以传入不同的类型,比如 new LinkedList,这样它的成员存放的类型也是 String。

为什么引入泛型

类型安全

泛型的主要目标是提高 Java 程序的类型安全
编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException
异常 符合越早出错代价越小原则

消除强制类型转换

泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
所得即所需,这使得代码更加可读,并且减少了出错机会

潜在的性能收益

由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
所有工作都在编译器中完成
编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一
样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,
也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,
这些类被称为参数化的类或参数化的类型。

public class Box<T> {
	private T t;
	public void add(T t) {
		this.t = t;
	}
	public T get() {
		return t;
 }

泛型方法( < E > )

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数
类型,编译器适当地处理每一个方法调用。
// 泛型方法 printArray

 public static < E > void printArray( E[] inputArray )
 { 
	 for ( E element : inputArray ){ 
	 	System.out.printf( "%s ", element );
	 }
 }
  1. <? extends T>表示该通配符所代表的类型是 T 类型的子类。
  2. <? super T>表示该通配符所代表的类型是 T 类型的父类。

泛型通配符?

泛型中有三种通配符形式:

<?> 无限制通配符 <? extends E> extends关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 接下来介绍各个通配符。

泛型的类型擦除

Java 中的泛型和 C++ 中的模板有一个很大的不同:

C++ 中模板的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。
Java中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。

在 Java 中,泛型是 Java 编译器的概念,用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。

实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。

当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。

实际上无论你是否使用泛型,集合框架中存放对象的数据类型都是 Object,这一点不仅仅从源码中可以看到,通过反射也可以看到。

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true

上面代码输出结果并不是预期的 false,而是 true。其原因就是泛型的擦除。

擦除的实现原理

一直有个疑问,Java 编译器在编译期间擦除了泛型的信息,那运行中怎么保证添加、取出的类型就是擦除前声明的呢?

Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。 当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。 总之泛型就是一个语法糖,它运行时没有存储任何类型信息。

擦除导致的泛型不可变性

泛型中没有逻辑上的父子关系,如 List 并不是 List 的父类。两者擦除之后都是List,所以形如下面的代码,编译器会报错:

/**
 * 两者并不是方法的重载。擦除之后都是同一方法,所以编译不会通过。
 * 擦除之后:
 * 
 * void m(List numbers){}
 * void m(List strings){} //编译不通过,已经存在相同方法签名
 */
void method(List<Object> numbers) {

}

void method(List<String> strings) {

}

泛型的这种情况称为 不可变性,与之对应的概念是 协变、逆变:
协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父类,则称之为协变的(父子关系保持一致)
逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变(放入容器就篡位了)
不可变:不论 A B 有什么关系,A 的容器和 B 的容器都没有父子关系,称之为不可变
Java 中数组是协变的,泛型是不可变的。
如果想要让某个泛型类具有协变性,就需要用到边界。

擦除的拯救者:边界

我们知道,泛型运行时被擦除成原始类型,这使得很多操作无法进行.
如果没有指明边界,类型参数将被擦除为 Object。
如果我们想要让参数保留一个边界,可以给参数设置一个边界,泛型参数将会被擦除到它的第一个边界(边界可以有多个),这样即使运行时擦除后也会有范围。

泛型的规则

泛型的参数类型只能是类(包括自定义类),不能是简单类型。
同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
泛型的类型参数可以有多个
泛型的参数类型可以使用extends 语句,习惯上称为“有界类型”
泛型的参数类型还可以是通配符类型,例如 Class

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三省同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值