预热:泛型

本文大量参考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 aHolder=new Holder<>();
    aHolder.setA("asd");
    aHolder.setA(new Object());
    
}
}```

创建Holder对象时可以指定泛型指向的对象,指明后就只能在Holder内部放入该类型(或其子类,多态与泛型不冲突),所以这里放入Object以及String都是可以的,Object是所有类的爹。

###泛型接口:同上

###泛型方法:

![泛型方法](http://upload-images.jianshu.io/upload_images/3267534-beeaebe61351a122.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
 方法可以独立于类而变化。无论何时,只要你能够做到,你就应该尽量使用泛型方法。在使用泛型类时我们创建对象需要去指定类型参数的值,而使用泛型方法的时候通常不需要明确指明,如上图,编译器会自动找出相应的值(type argument inference)。同样也可以显示指明类型:
            `response.f("123");`
    当然泛型方法与可变参数之间也是可以共存的:
 
 

public class Holder1 {

public static ArrayList test(T...a)
{
ArrayList result=new 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) {
Holder1holder1=new Holder1(new Test());
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) {
Holder1holder1=new Holder1(new Test1());
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 clazz;
public Holder1(Class class1) {
// 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(String.class).Test()));

}
}

output:[null, null]

  这里即使clazz被存储为Class,但是由于擦除,实际上也只是Class,因此在运行时Array.newInstance内的clazz没有实际含义。接上文描述的:在泛型代码内部是无法获得任何有关泛型参数类型的信息。所以这里Array.newInstance实际上并未拥有clazz蕴含的类型信息(这里的T没有实际意义,不知道实际上是什么)。
     而另一个例子;     

public class Holder1 {
private List list;
public Holder1() {
}
public List maker(T t,int n)
{
Listresult=new ArrayList<>();
for(int i=0;i {
result.add(t);
}
return result;
}

public static void main(String[] args) {
Holder1 sHolder1=new 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 "":()V
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
// or one of its superclasses.


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=new 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 "":()V
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[] gia;

public Holder1() {  
    
}

public static void main(String[] args) {
// gia=(Gener[]) new Object[10]; //编译器不会报错,但是运行会报错ClassCastException
// gia[0].gg();
gia=(Gener[]) new Gener[10];
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 genArr[] = new GenTest[2];
Object[] test = genArr;
GenTest strBuf = new GenTest();
strBuf.setValue(new StringBuffer());
test[0] = strBuf;
GenTest ref = genArr[0]; //上面两行相当于使用数组移花接木,让Java编译器把GenTest当作了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 void f(T t)
{
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 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但是这并不实际意味着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 list=new ArrayList();这种写法赋予了泛型一种协变性,
像之前提到过的:List list=new ArrayList()是无法通过的因为假如这么写,
那就意味着可以list.add(new Banana());这就破坏了list定义时的承诺,它是一个苹果列表。

那么针对这种情况(无法往内部添加)可以使用超类型通配符: ,有了超类型通配符,你就可以进行写入了:
![](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,f(B)=List,可是f(A)与f(B)不具备
任何关系,所以单纯的泛型不具备协变性。

所以ArrayList=ArrayList,(? extends fruit)<=fruit
,f(? extends fruit)=f(fruit),通过这种写法具备了协变性。

同样ArrayList=ArrayList,(? super fruit)>=fruit,
f(? extends fruit)=f(fruit),这就是逆变性。

#### 为什么要有协变逆变?优势在哪?
 而根据里氏替换原则:
 >派生类(子类)对象能够替换其基类(超类)对象被使用。当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

从例子来看:
 
 

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 list, int i) {
return list.get(i);
}

void put(List list, T item) {
    list.add(item);
}

void copy(List to, List from) {
    for(T item : from) {
        to.add(item);
    }
}

}

Car car = carUtils.get(benzs, 0);可以看出 List对List进行了替换(协变),

List的get方法返回Benz对象,而List返回的是Car对象,这符合替换原则,方法的后置条件(即方法的返回值)要比父类更严格。

carUtils.put(vehicles, new Car());List对List进行替换(逆变),

List的put方法需要Vehicle对象作为形参,而List需要的是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

你可能感兴趣的:(预热:泛型)