泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
其实就是一种编译检查的工具,类似某些注解一样, 如Override。
为什么说编译检查,例子
class A<E> {
public E f(E a) {
return a;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new A<String>().f(123)); // compile error
}
}
泛型的使用
泛型方法
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
public class Box<T> {
private T t;
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
}
}
泛型接口
public interface Generator<T> {
public T next();
}
在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
泛型的好处
① 类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作 java.lang.ClassCastException 展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性。
② 消除了代码中许多的强制类型转换,增强了代码的可读性。
③ 为较大的优化带来了可能。
类型通配符
为什么需要类型通配符
协变性:
- Java 数组是协变的:Integer[] 是 Number[] 的子类。 数组协变存在运行时风险:可能导致 ArrayStoreException
- 泛型是不可协变的:
List<Integer>不是List<Number>的子类。 泛型为了安全,默认是不可协变的,需要通过通配符支持(? extends T)
集合无协变性:
给
totalArea(Collection<Shape> arr)传递一个Collection<Circle>类型的集合,这是不可以的。编译器就会报如下的错误:The method totalArea(Collection<Shape>) in the type Demo is not applicable for the arguments (Collection<Circle>)
也就是说,形参是
List<Object>,你无法传入List<Integer>,List<Integer>,ArrayList<Integer>等,在编译期间就出现错误,它们是不同的类型。
有了类型通配符后,就可以约束泛型的父类或者子类,然后就可以让带有类型通配符泛型的 List 有数组一样的协变性,比如
//假如B继承A
List<B>可以赋值给List<? extends A>
List<? extends B>也可以赋值给List<? extends A>
注意如下是不可以的。以最终的泛型支持为主
The type List<?extends A>is assignable to List<A>.
类型通配符
类型通配符一般是使用?代替具体的类型参数。
<? extends T>和<? super T>的区别
前者表示该通配符所代表的类型是 T 类型的子类。
后者表示该通配符所代表的类型是 T 类型的父类。
<?> 是无界通配符。
注意不能能对List<? extends Fruit>的List进行Add. 会报错。 对于这个问题我们不妨从编译器的角度去考虑。List<? extends Fruit> 可能有多种含义,比如是ArrayList<Apple> 编译器无法识别所以会报错。 所以对于实现了<? extends T>的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。
class Fruit {
}
class Apple extends Fruit {
}
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Fruit()); // compile error: capture of ? extends Fruit
PECS 原则
规律
“Producer Extends” – 如果你需要一个只读 List,用它来 produce T,那么使用? extends 。 如果写会编译错误
- “Consumer Super” – 如果你需要一个只写 List,用它来 consume T,那么使用? super 。只允许写T,可读。
- 如果需要同时读取以及写入,那么我们就不能使用通配符了。
如果阅读过一些 Java 集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
记法: 子类赋值给父类。
泛型限定符有一描述:上界不存下界不取。
上界不存的原因:例如 List,编译器只知道容器内是 Father 及其子类,具体是什么类型并不知道,编译器在看到 extends 后面的 Father 类,只是标上一个 CAP#1 作为占位符,无论往里面插什么,编译器都不知道能不能和 CAP#1 匹配,所以就不允许插入。
extends的作用:可以在初始化的时候存入一个值,并且能保证数据的稳定性,只能取不能存。读取出来的数据可以存在父类或者基类里。
下界不取的原因:下界限定了元素的最小粒度,实际上是放松了容器元素的类型控制。例如 List, 元素是 Father 的基类,可以存入 Father 及其子类。但编译器并不知道哪个是 Father 的超类,如 Human。读取的时候,自然不知道是什么类型,只能返回 Object,这样元素信息就全部丢失了。
List<?>
The ?, or unbounded wildcard, means that the type of the object is not specified. It could be unknown, could be meant for multiple possible values or might be just plain irrelevant. List<?>, is pronounced “List of unknown.”
Unbounded Wildcards ?
The unbounded wildcard type is specified using the wildcard character (
?), for example,List<?>. This is called a list of unknown type. There are two scenarios where an unbounded wildcard is a useful approach:
- If you are writing a method that can be implemented using functionality provided in the Object class.
- When the code is using methods in the generic class that don’t depend on the type parameter. For example,
List.sizeorList.clear. In fact,Class<?>is so often used because most of the methods inClass<T>do not depend onT.
? 表示一种特定的未知类型,所以 List<?> 不能 add 因为会类型绑定错误而出现错误 capture xx of ?之类的。
List: There is no type restriction and assignment restriction at all.List<Object>: It seems to be used the same asList, but a compilation error will occur when accepting other generic assignments. 不能List<Object> list = a, where a is List<Integer>List<?>: It is a generic type. Before assignment, it means that it can accept any type of set assignment, but after assignment, you can’taddelements to it, but you canremoveandclear, not animmutable set.List<?>is generally used as a parameter to receive an external collection, or return a collection of specific element types, also known as awildcard collection.
The type List<A>is assignable to List.
The type List<Object>is assignable to List<?>.
注意,优先使用带?的泛型,这样做的好处是告诉编译器,我们是确实是采用任意类型的泛型,而非忘记使用泛型约束,并且在编译器检查时不会产生警告信息。
类型擦除
Java 的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。到了运行时,是看不到泛型的。Java的设计者在JDK 1.5时才引入了泛型,但为了照顾以前设计上的缺陷,同时兼容非泛型的代码,不得不做出了一个折中的策略:编译时对泛型要求严格,运行时却把泛型擦除了——要兼容以前的版本,还要升级扩展新的功能,真的很不容易!
泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
这意味着不管我们声明Node<String>还是Node<Integer>,到了运行期间,JVM统统视为Node<Object>。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
因此可能会带来一些问题,比如如下代码运行到最后一行时会产生异常 ClassCastException
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(2);
Object obj = intList;
//just warnning: Type safety: Unchecked cast from Object to ArrayList<String>
ArrayList<String> strList = (ArrayList<String>) obj;
// 这里会ClassCastException,因为类型擦除
//Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
String str = strList.get(0);
类型参数后
public static <E> void doX(List<E> args) {
System.out.println(args instanceof List<?>); // true
System.out.println(args instanceof List); // true
}
System.out.println(new ArrayList<String>().getClass() == (new ArrayList<Integer>().getClass())); // true
泛型的约束和限制
类型检查不可使用泛型
if(aaa instanceof Pair<String>){}//compile error: Illegal generic type for instanceof
Pair<String> p = (Pair<String>) a;//warn
Pair<String> p;
Pair<Integer> i;
i.getClass()==p.getClass();//true
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
可以这样使用
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
不能创建泛型对象数组
GenericMethod<User>[] o=null;//ok
o=new GenericMethod<User>[10];//error
可以定义泛型类对象的数组变量,不能创建及初始化。
注,可以创建通配类型数组,然后进行强制类型转换。不过这是类型不安全的。
o=(GenericMethod<User>[]) new GenericMethod<?>[10];
不可以创建的原因是:因为类型擦除的原因无法在为元素赋值时类型检查,因此 jdk 强制不允许。java对数组是有类型检查的,不论是写还是读。
不能实例化泛型对象
T t= new T();//error
T.class.newInstance();//error
T.class;//error
但可以这样创建
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
不能在泛型类的静态域中使用类泛型类型
public class Singleton<T>{
private static T singleton; //error
public static T getInstance(){} //error
public static void print(T t){} //error
}
但是,静态的泛型方法可以使用泛型类型:
public static <T> T getInstance(){return null;} //ok
public static <T> void print(T t){} //ok
这个原因很多资料中都没说的太明白,说一下个人理解,仅供参考:
- 泛型类中,
<T>称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:private T t;,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的。 - 静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T 所指的具体类型已经明确了。
继承泛型类时自动重写override
对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 Bridge method。
在继承一个泛型类时,会自动加对泛型类的方法的调用时的类型强制转换。
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
类型擦除后:
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {、
// 这个是关键!!!!,有个强制类型转化。
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
泛型实现原理
java泛型
基本问题:
定义一个泛型类最后到底会生成几个类,比如
ArrayList<T>到底有几个类- 只会生成一个
ArrayList.class,不会为每个不同的T生成单独的类。泛型参数T会在编译后被擦除,替换为它的擦除类型(通常是Object,或者是泛型上界)。
- 只会生成一个
定义一个泛型方法最终会有几个方法在class文件中
- 编译后,字节码中仍然只有一个方法
print(Object),因为T会被擦除为Object。
- 编译后,字节码中仍然只有一个方法
为什么泛型参数不能是基本类型呢
- 泛型参数会被替换为
Object,而基本类型(int、double)不是Object,无法直接替换
- 泛型参数会被替换为
```sh
ArrayList
是一个类吗 不是。
ArrayList<Integer>只是ArrayList<T>的一个实例化(泛型的应用)。由于类型擦除,
ArrayList<Integer>和ArrayList<String>在运行时是同一个类,都是ArrayList.class。
` ArrayList
和List 和ArrayList 和List `是什么关系呢,这几个类型的引用能相互赋值吗 ArrayList<Integer>可以 赋值给List<Integer>,因为ArrayList是List的子类。ArrayList<Integer>不能 赋值给ArrayList<Number>,因为ArrayList<Integer>不是ArrayList<Number>的子类,泛型不支持协变。List<Integer>不能 赋值给List<Number>,泛型是不变的(Invariant)。
```
泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。
Java 编译器通过如下方式实现擦除:
- 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
- 在恰当的位置插入强制转换代码来确保类型安全;
- 在继承了泛型类或接口的类中插入桥接方法来保留多态性。
java 编译后不同类型的模板类编译出的是同一份代码。然后在使用时编译器会帮助进行类型转换.
所以 java 泛型的实现是在运行时去进行判断和类型转换的,这样会对运行时的效率有一定影响,但编译出来的泛型类的代码只需要一份.
Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀(C++模板令人困扰的难题)的问题,但是也引起了许多新的问题。所以,Sun对这些问题作出了许多限制,避免我们犯各种错误。
首先第一个是泛型所宣称的类型安全,既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
- 编译器生成的class文件中会在你调用泛型方法完成之后返回调用点之前加上类型转换的操作,比如上文的get函数,就是在get方法完成后,jump回原本的赋值操作的指令位置之前加入了强制转换,转换的类型由编译器推导。
C++泛型
c++ 的泛型实现机制很简单,就是在实际编译时,类似于宏一样,把实际的类型代入模板,并针对不同的类型生成不同的代码,所以编译后代码体积会变大,但执行时就不需要额外的判断了,运行时效率会较高. 所以 c++ 的泛型可以说是以空间换时间.
例如 std::list 容器,如果你将 int 类型的数据存进去,C++ 编译器就为你生成一个专门用来存 int 类型数据的列表数据结构。也就是说,你向 std::list 容器中存放什么类型,C++ 编译器就为你生成相应的列表数据结构。理论上,数据的类型是无限的,因此 C++ 要生成的列表数据结构也是无限的。如果你的程序中有大量的数据类型要存到 std::list 容器,那么代码就会高度膨胀,这种膨胀是 C++ 编译器在目标文件连接阶段无法优化的。