本文大量参考Thinking in java(解析,填充)。
定义:多态算是一种泛化机制,解决了一部分可以应用于多种类型代码的束缚。虽然我们可以在参数定义一个基类或者是一个接口,但是他们的约束还是太强了,有的时候我们更希望编写更通用的代码,使代码能够应用于“某种不具体的类型”.而正是泛型的出现解决了这类问题,它实现了参数化类型的概念。其最初的目的是希望类或方法能够具备最广泛的表达能力(通过解耦类或方法与所使用的 类型之间的约束)。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类的正确性。
泛型参数不能使用基本类型.
泛型类:
public class Holder {
private T a;
public T getA() {
return a;
}
public void setA(T a) {
this.a = a;
}
public static void main(String[] args) {
Holder
public class Holder1 {
public static
{
ArrayList
for(T item:a)
{
result.add(item);
}
return result;
}
public static void main(String[] args) {
System.out.println(test("123","234","345"));
}
}
output:[123,234,345]
###擦除:
尽管可以声明Arraylist.class但是无法声明ArrayList.class.
Class class1=new ArrayList().getClass();
Class class2=new ArrayList().getClass();
System.out.println(class1==class2);
output:true
通过这种方式可以看的更清楚:
public class Holder1 {
class A{};
class B{};
public static void main(String[] args) {
List list =new ArrayList();
HashMap hashMap=new HashMap<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(hashMap.getClass().getTypeParameters()));
}
}
output:
[E]
[K, V]
Class.getTypeParameters()将返回一个TypeVariavle对象数组,表示有泛型声明所声明的类型参数。然而输出的只是标识符,所以可以得出以下结论:
在泛型代码内部是无法获得任何有关泛型参数类型的信息。
尽管你知道类型参数标识符和泛型类型边界这类的信息--你却无法知道用来创建某个特定实例的实际类型参数。
这就是擦除带来的弊端。比如说:
public class Holder1
private T obj;
public Holder1(T obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public void test()
{
//obj.test(); wrong
}
public T getObj()
{
return obj;
}
public static void main(String[] args) {
Holder1
holder1.test();
}
}
class Test{
public void test()
{
System.out.println("sb");
}
}
由于擦除,这种调用在java中是无法实现的,而为了实现这种需求(obj需要调用f())就有了extends关键字,也就是泛型的边界
extends:
` public class Holder1`
声明T必须具有类型Test或者从Test导出的类型.也就是当前定义的泛型必须是Test或其子类.
为什么通过这样就能成功实现我们的需求?因为泛型类型参数在运行时是擦除到它的第一个边界,编译器实际上会把类型参数替换为它的最后擦除类,所以当前的T在擦除后实际上是Test,等于做了一个替换。
那么这么做的意义在哪里,和我这样去写的差别在哪:
public class Holder1 {
private Test obj;
public Holder1(Test obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public Test getObj() {
return obj;
}
public void test()
{
obj.test();
}
}
根本原因是通过泛化,能让当前代码跨越多个类工作,它不明确定义某个字类型,在使用时能返回确切的类型信息。比如:
这里返回的就是我定义的Test1
public class Holder1
private T obj;
public Holder1(T obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public T getObj() {
return obj;
}
public void test()
{
obj.test();
}
public static void main(String[] args) {
Holder1
holder1.getObj();//这里返回的就是我定义的Test1对象
}
}
class Test{
public void test()
{
System.out.println("sb");
}
}
class Test1 extends Test
{
}
为什么会有擦除,而不能像C++一样实现完整的泛化机制:
> 这就是为了保证向前的兼容性,java早期并没有泛型的相关概念,并且能够减少JVM相关的改变,以及不破坏现有类库的前提下,以最小代价来实现相关概念。
而这也使得泛型在java当中不是那么好用。所以在运行时期,所有泛型都将被擦除,替换成它们的非泛型上界,例如List这种将被擦除为List,而普通的类型变量在没定义边界的情况下被擦除为Object.
####擦除的代价:
不能用于显式地引用运行时类型的操作之中:转型(cast)、instanceof操作和new表达式。因为所有的参数类型信息在运行时期都会丢失,所以需要无时无刻提醒自己:参数的类型信息只是目前看起来拥有而已。最后只会留下它的上界。
###边界处的动作:
public class Holder1
private Class
public Holder1(Class
// TODO Auto-generated constructor stub
clazz=class1;
}
public T[] Test() {
return (T[]) Array.newInstance(clazz, 2);//这里强转,并且有cast警告
}
public static void main(String[] args) {
System.out.println(Arrays.toString(new Holder1
}
}
output:[null, null]
这里即使clazz被存储为Class,但是由于擦除,实际上也只是Class,因此在运行时Array.newInstance内的clazz没有实际含义。接上文描述的:在泛型代码内部是无法获得任何有关泛型参数类型的信息。所以这里Array.newInstance实际上并未拥有clazz蕴含的类型信息(这里的T没有实际意义,不知道实际上是什么)。
而另一个例子;
public class Holder1
private List
public Holder1() {
}
public List
{
List
for(int i=0;i
result.add(t);
}
return result;
}
public static void main(String[] args) {
Holder1
System.out.println(sHolder1.maker("asd", 6));
}
}
这个例子中,尽管在运行时会擦除所有T类型的相关信息,可是它仍旧可以确保在编译器你放置到Holder1当中的对象具有T类型,使其适合List,确保了在方法或类中的类型内部一致性,这也可以认为是一种规法。不过还是那句话,内部并不知道T的实际含义.只能确保类型的统一.
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:对象进入和离开方法的地点。
看下面两个例子:
1.不使用泛型
public class Holder1 {
private Object object;
public Holder1() {
}
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
public static void main(String[] args) {
Holder1 holder1=new Holder1();
holder1.setObject("String");
String string=(String) holder1.getObject();
}
}
反编译后:
D:>javap -c Holder1
Compiled from "Holder1.java"
public class Holder1 {
public Holder1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
()V
4: return
public void setObject(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field object:Ljava/lang/Object;
5: return
public java.lang.Object getObject();
Code:
0: aload_0
1: getfield #2 // Field object:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class Holder1
3: dup
4: invokespecial #4 // Method "
7: astore_1
8: aload_1
9: ldc #5 // String String
11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
ct;)V
14: aload_1
15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
ect;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
前面的一大部分省略,set和get都是针对Object操作,观察第18行得知,get之后会有一个checkcast类型检查,翻阅
Java Virtual Machine Online Instruction Reference:得知
> checkcast:ensure type of an object or array, checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type.
翻译:确保一个Object或者Array的类型。检查操作数栈的最上层item(object或array的引用)是否能被转换成相应的类型。
checkcast 实际上可以被认为是:
if (! (obj == null || obj instanceof
throw new ClassCastException();
}
// if this point is reached, then object is either null, or an instance of
//
2.使用泛型:
public class Holder1
private T object;
public Holder1() {
}
public void setObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
public static void main(String[] args) {
Holder1
holder1.setObject("String");
String string=holder1.getObject();
}
}
反编译后:
public static void main(java.lang.String[]);
Code:
0: new #3 // class Holder1
3: dup
4: invokespecial #4 // Method "
7: astore_1
8: aload_1
9: ldc #5 // String String
11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
ct;)V
14: aload_1
15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
ect;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
11行setObject开始传入的就是Object,但是set()方法不需要类型检查,编译器已经检查过了,但是对get方法在18行checkcast还是进行了类型检查,只不过用了泛型以后由编译器自动插入,其实效果是一样的。
在泛型中所有动作发生在边界处---对传进来的值做额外的编译期检查,并由编译器插入传出去的值的转型。这都是在编译期间完成的。
###泛型数组:
class Gener
{
public void gg()
{
System.out.println("ASD");
}
}
public class Holder1 {
static Gener
public Holder1() {
}
public static void main(String[] args) {
// gia=(Gener
// gia[0].gg();
gia=(Gener
gia[0]=new Gener
gia[0].gg();
}
}
那么既然数组无论它们持有的类型如何,都具有相同的结构,看起来是可以创建一个Object数组并将其转型为所希望的数组类型。事实上这样做会报错,为什么呢。
通过编写下述代码,进行反编译
public class TT {
public static void main(String[] args) {
int[] aa=new int[10];
String[]bb=new String[10];
}
}
获得
public class TT {
public TT();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10 //push the int 10 onto the stack,将10放入堆栈中
2: newarray int
4: astore_1
5: bipush 10
7: anewarray #2 // class java/lang/String
10: astore_2
11: return
}
这里有个newarray和anewarray就是创建数组,同样翻阅
Java Virtual Machine Online Instruction Reference:
>newarray:allocate new array for numbers or booleans.
newarray is used to allocate single-dimension arrays of booleans, chars, floats, doubles, bytes, shorts, ints or longs.
newarray pops a positive int, n, off the stack, and constructs an array for holding n elements of the type given by . Initially the elements in the array are set to zero. A reference to the new array object is left on the stack.
翻译:newarray给numbers或者booleans分配一个新的数组。它被用来分配booleans, chars, floats, doubles, bytes, shorts, ints or longs. 的一维数组。
newarray从堆栈中弹出一个正整数n,然后构造一个type是你定义的类型的数组。初始化数组当中所有的元素(都设置默认为0)。在堆栈上留下对新数组对象的引用.
>anewarray:allocate new array for objects. is either the name of a class or interface, e.g. java/lang/String, or, to create the first dimension of a multidimensional array, can be an array type descriptor, e.g. [Ljava/lang/String;
anewarray allocates a new array for holding object references. It pops an int, size, off the stack and constructs a new array capable of holding size object references of the type indicated by .
indicates what types of object references are to be stored in the array (see aastore). It is the name of a class or an interface, or an array type descriptor. If it is java/lang/Object, for example, then any type of object reference can be stored in the array. is resolved at runtime to a Java class, interface or array. See Chapter 7 for a discussion of how classes are resolved.
A reference to the new array is pushed onto the stack. Entries in the new array are initially set to null.
翻译:给对象分配新的数组.可以是class或者interface的名称,比如说java/lang/String, 或者为了创建多维数组的第一维,可以是数组类型描述符,例如 [Ljava/lang/String; anewarray 分配一个新的数组去持有对象的引用。它从对战中弹出一个int类型的size(大小),并构造一个能够持有size个type对象引用的新数组。
表示object引用在数组当中是以什么类型被存储的。它是class或者interface的名称或者数组类型的描述。比如说type是 java/lang/Object,那么任意object引用都能被存储进当前数组中,而在运行时将解析成java类,interface或者数组。
一个新的数组引用将push到堆栈上,数组中所有条目都会被设置为null
从这里就可以看出,这里泛型数组的构建会调用anewarray,而anewarray需要明确的type,
那么这样就可以知道:
1.在gia=(Gener[]) new Object[10]; 中,即使gia看起来是转型为Gener[],但是这也只是在编译期,运行时他仍然是Object[],正是因为anewarray运行时已经将type定义为Object,你无法对底层的数组进行更改。所以强制转型会引起ClassCastException.
2.根据Oracle的java文档来看,泛型属于Non_Reifiable type,而引起这部分的原因也是因为类型擦除Non_Reifiable type会在编译期被移除泛型信息,所以在运行时无法获取具体的类型信息。而java明确规定数组内的元素必须是reifiable的,所以类似T[] a=new T[10]这类型的无法通过编译。
参考例子:
String[] strArray = new String[20];
Object[] objArray = strArray;
objArray[0] = new Integer(1); // throws ArrayStoreException at runtime
那么假如说泛型的数组可以直接创建:
ArrayList[] a=new ArrayList[];
那么随后也可以改为Object数组然后往里面放ArrayList,我们在随后的代码中可以把它转型为Object[]然后往里面放Arraylist实例。
这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Arraylist的对象,我们定义的在这个时候已经被抹掉了.
//下面的代码使用了泛型的数组,是无法通过编译的
GenTest
Object[] test = genArr;
GenTest
strBuf.setValue(new StringBuffer());
test[0] = strBuf;
GenTest
String value = ref.getValue();// 这里是重点!
最后一行中,根据之前讲到的泛型边界问题,取值的时候会是这样
(String)ref.getValue();所以会有ClassCastException.这个程序虽然看起来是程序员的错误,
而且也没有什么灾难性后果。但是从另一个角度看,泛型就是为了消灭ClassCastException出现的
而这个时候他自己却引发这个错误,这就矛盾了。通常来说如果使用泛型,只要代码编译时没有警告,那么就不会出现错误ClassCaseException。
究竟泛型数组应该怎么用,我们可以参考ArrayList的源码
transient Object[] elementData; // non-private to simplify nested class access
它内部使用的就是Object[]
get方法对item使用了强转,才能让我们获取到正确的对象。
// Positional Access Operations
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
add方法涉及到向上转型
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
事实证明,ArrayList大量使用了强转去实现。
总的来说数组是协变的而泛型不是,如果 Integer扩展了 Number(事实也是如此),那么不仅 Integer是 Number,
而且 Integer[]也是 Number[],在要求 Number[]的地方完全可以传递或者赋予 Integer[]。
这也是矛盾发生的根本原因.
###边界:
简单点就是 这样在擦除的时候T就会转换成Base,而在泛型类或方法中可以直接视同Base
的方法
public class Response {
public
{
t.test();
}
}
class ABC{
public void test()
{
System.out.println("asdasd");
}
}
另一种用法就是
interface has{
void test();
}
public class Ges
T mT;
public T getmT() {
return mT;
}
public void setmT(T mT) {
this.mT = mT;
}
继承一个父类与多个接口的形式。和类继承的用法是一样的,不过class必须在第一个,接口跟在后面
同样,如果希望将某个类型限制为特定类型或特定类型的超类型,请使用以下表示法:
###通配符:
泛型没有内建的协变类型,有时候想要在两个类型之间建立某种类型的向上转型关系:这正是通配符允许的
public class Ges {
public static void main(String[] args) {
List extends fruit> list=new ArrayList
// list.add(new fruit());
// list.add(new apple()); all wrong
fruit fruit=list.get(0);//可以获取到fruit
}
}
class fruit{
}
class apple extends fruit{
}
尽管list类型是List extends fruit>但是这并不实际意味着List将持有任何类型的fruit。实际上你不能往这个list当中安全的添加对象。尽管他可以合法指向一个list,
你无法往里面丢任何对象。编译器只知道list内部的任何对象至少具有fruit类型,但是他具体是什么就不知道了。
每当指定add方法的时候,![](http://upload-images.jianshu.io/upload_images/3267534-3002ac82f9d98063.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)显示的都是null,由于不知道,所以干脆不接受任何类型的fruit。同样的编译器将直接拒绝对参数列表中
涉及通配符的方法的调用。
当然List extends fruit> list=new ArrayList();这种写法赋予了泛型一种协变性,
像之前提到过的:List list=new ArrayList()是无法通过的因为假如这么写,
那就意味着可以list.add(new Banana());这就破坏了list定义时的承诺,它是一个苹果列表。
那么针对这种情况(无法往内部添加)可以使用超类型通配符: super Class> ,有了超类型通配符,你就可以进行写入了:
![](http://upload-images.jianshu.io/upload_images/3267534-dd793ecf68699d44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
这相当于定义了一个下界,无论你传入什么,最起码是apple,或者是他的子类,这样的类型传入是安全的.
这种写法也可以称之为逆变,同样的get方法返回的是Object.
###关于协变和逆变:
什么是协变(逆变):
>如果A和B是类型(T),f表示类型转换,≤表示子类型关系,(例如A≤B,表示A是B的子类)那么:
如果A≤B 则f(A) ≤ f(B) 那么 f是协变的 (假如说T和f(T)序关系一致,就是协变)
如果A≤B 则f(B) ≤ f(A) 那么 f 是逆变的(假如说T和f(T)序关系相反,就是逆变)
如果上面两种都不成立,那么f是无关的
例如:class List{...},可以理解为输入一个类型参数T,通过类型转换(f)成为,List(f(T))
所以当A=Object,B=String,那么f(A)=List
public class f {
// 定义三个类: Benz -> Car -> Vehicle,它们之间是顺次继承关系
// 测试函数
void test() {
List vehicles = new ArrayList<>();
List benzs = new ArrayList<>();
Utils carUtils = new Utils<>();
carUtils.put(vehicles, new Car());
Car car = carUtils.get(benzs, 0);
carUtils.copy(vehicles, benzs);
}
}
class Vehicle {}
class Car extends Vehicle {}
class Benz extends Car {}
// 定义一个util类,其中用到泛型里的协变和逆变
class Utils
T get(List extends T> list, int i) {
return list.get(i);
}
void put(List super T> list, T item) {
list.add(item);
}
void copy(List super T> to, List extends T> from) {
for(T item : from) {
to.add(item);
}
}
}
Car car = carUtils.get(benzs, 0);可以看出 List对List extends Car>进行了替换(协变),
List的get方法返回Benz对象,而List extends Car>返回的是Car对象,这符合替换原则,方法的后置条件(即方法的返回值)要比父类更严格。
carUtils.put(vehicles, new Car());List对List super Car>进行替换(逆变),
List的put方法需要Vehicle对象作为形参,而List super Car>需要的是Car,这就满足替换原则
的前置条件需求.
最后一个copy体现的是协变与逆变的汇总,替换。所以总的来说泛型的协变与逆变定义上界与下界,同时也让程序
能够在某种程度上满足替换原则,通过良好的替换让程序更具拓展性。这也是为什么大量的框架中
例如rxjava,会在参数中大量使用这种形式的泛型写法。
参考:
Thinking in java
http://ybin.cc/programming/java-variance-in-generics/
https://www.zybuluo.com/zhanjindong/note/34147
http://www.cnblogs.com/en-heng/p/5041124.html
http://colobu.com/2015/05/19/Variance-lower-bounds-upper-bounds-in-Scala/#Java中的协变和逆变
http://blog.csdn.net/hopeztm/article/details/8822545
https://zh.wikipedia.org/wiki/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99
https://www.ibm.com/developerworks/cn/java/j-jtp01255.html
https://www.zhihu.com/question/20928981
http://cs.au.dk/~mis/dOvs/jvmspec/ref-Java.html
http://www.blogjava.net/deepnighttwo/articles/298426.html