首先提个问题:
Java 泛型的做用是什么?泛型擦除是什么?泛型通常用在什么场景?html若是这个问题你答不上来,那这篇文章可能就对你有些价值。java
读完本文你将了解到:android
泛型是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>
类名及其实现的接口名后有个特殊的部分 “”,并且它的成员的类型 Link<E>
也包含一个 “”,这个符号的就是 类型参数,它使得在运行中,建立一个 LinkedList 时能够传入不一样的类型,好比 new LinkedList,这样它的成员存放的类型也是 String。bash
在引入泛型以前,要想实现一个通用的、能够处理不一样类型的方法,你须要使用 Object 做为属性和方法参数,好比这样:框架
public class Generic {
private Object[] mData;
public Generic(int capacity) {
mData = new Object[capacity];
}
public Object getData(int index) {
//...
return mData[index];
}
public void add(int index, Object item) {
//...
mData[index] = item;
}
}
复制代码
它使用一个 Object
数组来保存数据,这样在使用时能够添加不一样类型的对象:编辑器
Generic generic = new Generic(10);
generic.add(0,"shixin");
generic.add(1, 23);
复制代码
然而因为 Object
是全部类的父类,全部的类均可以做为成员被添加到上述类中;当须要使用的时候,必须进行强制转换,并且这个强转颇有可能出现转换异常:
String item1 = (String) generic.getData(0);
String item2 = (String) generic.getData(1);
复制代码
上面第二行代码将一个 Integer 强转成 String,运行时会报错 :
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at net.sxkeji.shixinandroiddemo2.test.generic.GenericTest.getData(GenericTest.java:46)
复制代码
能够看到,使用 Object 来实现通用、不一样类型的处理,有这么两个缺点:
根据《Java 编程思想》中的描述,泛型出现的动机在于:
有许多缘由促成了泛型的出现,而最引人注意的一个缘由,就是为了建立容器类。
事实上,在 JDK 1.5 出现泛型之后,许多集合类都使用泛型来保存不一样类型的元素,好比 Collection:
public interface Collection<E> extends Iterable<E> {
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
}
复制代码
实际上引入泛型的主要目标有如下几点:
泛型的本质是参数化类型,也就是说所操做的数据类型被指定为一个参数。
类型参数的意义是告诉编译器这个集合中要存放实例的类型,从而在添加其余类型时作出提示,在编译时就为类型安全作了保证。
这种参数类型能够用在类、接口和方法的建立中,分别称为泛型类、泛型接口、泛型方法。
/**
* <header>
* Description: 泛型类
* </header>
* <p>
* Author: shixinzhang
*/
public class GenericClass<F> {
private F mContent;
public GenericClass(F content){
mContent = content;
}
/**
* 泛型方法
* @return
*/
public F getContent() {
return mContent;
}
public void setContent(F content) {
mContent = content;
}
/**
* 泛型接口
* @param <T>
*/
public interface GenericInterface<T>{
void doSomething(T t);
}
}
复制代码
泛型类和普通类的区别就是类名后有类型参数列表 <E>
,既然叫“列表”了,固然这里的类型参数能够有多个,好比 public class HashMap<K, V>
,参数名称由开发者决定。
类名中声明参数类型后,内部成员、方法就可使用这个参数类型,好比上面的 GenericClass<F>
就是一个泛型类,它在类名后声明了类型 F,它的成员、方法就可使用 F 表示成员类型、方法参数/返回值都是 F 类型。
泛型类最多见的用途就是做为容纳不一样类型数据的容器类,好比 Java 集合容器类。
和泛型类同样,泛型接口在接口名后添加类型参数,好比上面的 GenericInterface<T>
,接口声明类型后,接口方法就能够直接使用这个类型。
实现类在实现泛型接口时须要指明具体的参数类型,否则默认类型是 Object,这就失去了泛型接口的意义。
未指明类型的实现类,默认是 Object 类型:
public class Generic implements GenericInterface{
@Override
public void doSomething(Object o) {
//...
}
}
复制代码
指明了类型的实现:
public class Generic implements GenericInterface<String>{
@Override
public void doSomething(String s) {
//...
}
}
复制代码
泛型接口比较实用的使用场景就是用做策略模式的公共策略,好比 Java 解惑:Comparable 和 Comparator 的区别 中介绍的 Comparator,它就是一个泛型接口:
public interface Comparator<T> {
public int compare(T lhs, T rhs);
public boolean equals(Object object);
}
复制代码
泛型接口定义基本的规则,而后做为引用传递给客户端,这样在运行时就能传入不一样的策略实现类。
泛型方法是指使用泛型的方法,若是它所在的类是个泛型类,那就很简单了,直接使用类声明的参数。
若是一个方法所在的类不是泛型类,或者他想要处理不一样于泛型类声明类型的数据,那它就须要本身声明类型,举个例子:
/**
* 传统的方法,会有 unchecked ... raw type 的警告
* @param s1
* @param s2
* @return
*/
public Set union(Set s1, Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
/**
* 泛型方法,介于方法修饰符和返回值之间的称做 类型参数列表 <A,V,F,E...> (能够有多个)
* 类型参数列表 指定参数、返回值中泛型参数的类型范围,命名惯例与泛型相同
* @param s1
* @param s2
* @param <E>
* @return
*/
public <E> Set<E> union2(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
复制代码
注意上述代码在返回值前面也有个 <E>
,它和类名后面的类型参数列表意义一致,指明了这个方法中类型参数的意义、范围。
有时候但愿传入的类型有一个指定的范围,从而能够进行一些特定的操做,这时候就是通配符边界登场的时候了。
泛型中有三种通配符形式:
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型多是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型多是指定的类型,或者是此类型的父类
复制代码
接下来介绍各个通配符。
要使用泛型,可是不肯定或者不关心实际要操做的类型,可使用无限制通配符(尖括号里一个问号,即 <?>
),表示能够持有任何类型。
大部分状况下,这种限制是好的,但这使得一些理应正确的基本操做都没法完成,好比交换两个元素的位置,看代码:
private void swap(List<?> list, int i, int j){
Object o = list.get(i);
list.set(j,o);
}
复制代码
这个代码看上去应该是正确的,但 Java 编译器会提示编译错误,set 语句是非法的。编译器提示咱们把方法中的 List<?>
改为 List<Object>
就行了,这是为何呢? ?
和 Object
不同吗?
的确由于 ?
和 Object
不同,List<?>
表示未知类型的列表,而 List<Object>
表示任意类型的列表。
好比传入个 List<String>
,这时 List 的元素类型就是 String
,想要往 List
里添加一个 Object
,这固然是不能够的。
借助带类型参数的泛型方法,这个问题能够这样解决:
private <E> void swapInternal(List<E> list, int i, int j) {
//...
list.set(i, list.set(j, list.get(i)));
}
private void swap(List<?> list, int i, int j){
swapInternal(list, i, j);
}
复制代码
swap
能够调用 swapInternal
,而带类型参数的 swapInternal
能够写入。Java容器类中就有相似这样的用法,公共的 API 是通配符形式,形式更简单,但内部调用带类型参数的方法。
(这个例子引自: mp.weixin.qq.com/s/te9K3alu8… )
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
举个例子:
/**
* 有限制的通配符之 extends (有上限),表示参数类型 必须是 BookBean 及其子类,更灵活
* @param arg1
* @param arg2
* @param <E>
* @return
*/
private <K extends ChildBookBean, E extends BookBean> E test2(K arg1, E arg2){
E result = arg2;
arg2.compareTo(arg1);
//.....
return result;
}
复制代码
能够看到,类型参数列表中若是有多个类型参数上限,用逗号分开。
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
根据代码介绍吧:
private <E> void add(List<? super E> dst, List<E> src){
for (E e : src) {
dst.add(e);
}
}
复制代码
能够看到,上面的 dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,所以装得下 dst 的容器也就能装 src。
经过上面的例子咱们能够知道,无限制通配符 < ?> 和 Object 有些类似,用于表示无限制或者不肯定范围的场景。
两种有限制通配形式 < ? super E> 和 < ? extends E> 也比较容易混淆,咱们再来比较下。
它们的目的都是为了使方法接口更为灵活,能够接受更为普遍的类型。
用《Effective Java》 中的一个短语来加深理解:
为了得到最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限:
PECS: producer-extends, costumer-super
所以使用通配符的基本原则:
小总结一下:
举个例子:
private <E extends Comparable<? super E>> E max(List<? extends E> e1){
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
复制代码
上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>
,咱们能够分步查看:
Java 中的泛型和 C++ 中的模板有一个很大的不一样:
(摘自:blog.csdn.net/fw0124/arti…)
在 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 编译器在编译期间擦除了泛型的信息,那运行中怎么保证添加、取出的类型就是擦除前声明的呢?
从这篇文章了解到,原来泛型也只是一个语法糖,摘几段话加深理解:
The buzzing keyword is “Type Erasure”, you guessed it right it’s the same thing we used to in our schools for erasing our mistakes in writing or drawing :).
The Same thing is done by Java compiler, when it sees code written using Generics it completely erases that code and convert it into raw type i.e. code without Generics. All type related information is removed during erasing. So your ArrayList becomes plain old ArrayList prior to JDK 1.5, formal type parameters e.g. < K, V> or < E> gets replaced by either Object or Super Class of the Type.
Also, when the translated code does not have correct type, the compiler inserts a type casting operator. This all done behind the scene so you don’t need to worry about what important to us is that Java compiler guarantees type-safety and flag any type-safety relate error during compilation.
In short Generics in Java is syntactic sugar and doesn’t store any type related information at runtime. All type related information is erased by Type Erasure, this was the main requirement while developing Generics feature in order to reuse all Java code written without Generics.
大概意思就是:
Java 编辑器会将泛型代码中的类型彻底擦除,使其变成原始类型。
固然,这时的代码类型和咱们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操做都是编译器后台进行,能够保证类型安全。
总之泛型就是一个语法糖,它运行时没有存储任何类型信息。
泛型中没有逻辑上的父子关系,如 List 并非 List 的父类。二者擦除以后都是List,因此形以下面的代码,编译器会报错:
/**
* 二者并非方法的重载。擦除以后都是同一方法,因此编译不会经过。
* 擦除以后:
*
* void m(List numbers){}
* void m(List strings){} //编译不经过,已经存在相同方法签名
*/
void method(List<Object> numbers) {
}
void method(List<String> strings) {
}
复制代码
泛型的这种状况称为 不可变性,与之对应的概念是 协变、逆变:
Java 中数组是协变的,泛型是不可变的。
若是想要让某个泛型类具备协变性,就须要用到边界。
咱们知道,泛型运行时被擦除成原始类型,这使得不少操做没法进行.
若是没有指明边界,类型参数将被擦除为 Object。
若是咱们想要让参数保留一个边界,能够给参数设置一个边界,泛型参数将会被擦除到它的第一个边界(边界能够有多个),这样即便运行时擦除后也会有范围。
好比:
public class GenericErasure {
interface Game {
void play();
}
interface Program{
void code();
}
public static class People<T extends Program & Game>{
private T mPeople;
public People(T people){
mPeople = people;
}
public void habit(){
mPeople.code();
mPeople.play();
}
}
}
复制代码
上述代码中, People 的类型参数 T 有两个边界,编译器事实上会把类型参数替换为它的第一个边界的类型。
当类中要操做的引用数据类型不肯定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。
1.上面说到使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优点,那为何 ArrayList 等源码中的还能看到使用 Object 做为类型?
根据《Effective Java》中所述,这里涉及到一个 “移植兼容性”:
泛型出现时,Java 平台即将进入它的第二个十年,在此以前已经存在了大量没有使用泛型的 Java 代码。人们认为让这些代码所有保持合法,而且可以与使用泛型的新代码互用,很是重要。
这样都是为了兼容,新代码里要使用泛型而不是原始类型。
2.泛型是经过擦除来实现的。所以泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码能够和没有使用泛型的代码随意互用。
3.若是类型参数在方法声明中只出现一次,能够用通配符代替它。
好比下面的 swap 方法,用于交换指定 List 中的两个位置的元素:
private <E> void swap(List<E> list, int i, int j) {
//...
}
复制代码
只出现了一次 类型参数,没有必要声明,彻底能够用通配符代替:
private void swap(List<?> list, int i, int j){
//...
}
复制代码
对比一下,第二种更加简单清晰吧。
4.数组中不能使用泛型
这多是 Java 泛型面试题中最简单的一个了,固然前提是你要知道 Array 事实上并不支持泛型,这也是为何 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,由于 List 能够提供编译期的类型安全保证,而 Array 却不能。
5.Java 中 List<Object>
和原始类型 List
之间的区别?
原始类型和带参数类型 之间的主要区别是:
这道题的考察点在于对泛型中原始类型的正确理解。