java中泛型的理解

       回首望来做了程序员也有些年头了,一直浑浑噩噩,从事Android也有三年多了,最近回头一看自己还是什么都不会,觉得自己还是一个小白,可能跟自己的半路出家非计算机专业出身,以及基础不扎实导致,所以决定要重新学习以前的知识。重新记录一下自己学习的心得。做个总结。


虽然现在有了kotlin 但是也是要编译成为java去运行,所以还是要提升自己的java语言基础。java中的泛型算是一个基础性的东西了,要想写一些通用的东西是必不可少的,首先了解一下概念

背景:

Java集合(Collection)中元素的类型是多种多样的。例如,有些集合中的元素是Byte类型的,而有些则可能是String类型的,等等。Java允许程序员构建一个元素类型为Object的Collection,其中的元素可以是任何类型在Java SE 1.5之前,没有泛型(Generics)的情况下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。因此,为了解决这一问题,J2SE 1.5引入泛型也是自然而然的了。

作用:

第一是泛化。可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

第二是类型安全。泛型的一个主要目标就是提高ava程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。

第三是消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

第四是向后兼容。支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。

ps:摘抄自百度百科...


学习主要从以下几个方面来总结一下:
1.我们为什么要泛型
2.泛型类,泛型接口,泛型方法
3.如何限定类型变量
4.泛型使用中的约束和局限性
5.泛型类型能继承吗?
6.泛型中通配符类型
7.虚拟机是如何实现泛型的

一:我们为什么要泛型:

1.适用于多种数据类型执行相同的代码

举个栗子:

public int addInt(int a,int b){ 
 return a+b;
}

public double addDouble(double a,double b){ 
return a+b;
}

       这是一段很普通的代码就是a+b的一个公式计算,但是当你确定只是需要int值计算的时候是没有问题的,但是当你需要double,float,long,等的值计算的时候,显然就不是那么合适了,需要去重新copy,然后去一个一个的修改,改返回值类型,改输入值的类型。做起来就有点费时费力了,有些人可能会说我时间多,我不嫌烦,那么你可以去cv去改,咱这只是一个小的例子,当你方法比较复杂,而且多变呢?当然这个地方只是举个栗子,真实情况不一定使用泛型就能够实现需求。这里只是用这个栗子来说泛型的一种思想

2.泛型中的类型在使用时指定,不需要强制类型转换,如果出现数据类型错误,在编辑期间就可以发现,不至于在运行期间才可以发现错误。

  List list = new ArrayList();

  list.add("hello");

  list.add("world");

  list.add(5);

  for (int i = 0; i < list.size(); i++) {

     String name = (String) list.get(i);

    System.out.println("name "+name);

   }

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

       很简单的一段代码,首先创建一个集合,然后向集合中先加入两个字符串,然后再加入一个数字,然后去遍历整个list,可以看到在编译期是没有问题,但是当运行的时候会发现报错,报一个Integer不能转换成String的错误,list默认类型为Object,因此加入数据的时候是没有问题的,可是在工作当中,由于粗心等原因会忘记了之前添加过integer的值所以导致了后期在运行的时候转换时出错,而且此问题还不易被发现。
    在上边的编码中主要存在两个问题:
1.当我们将一个对象放入集合中时,集合不会去记住放入对象的类型,当再次从集合中取出值时,会默认成object类型,但是它本身实际类型还是其本身类型
2.因此取出集合元素时需要人为的去转换类型到目标实际类型,但是很容易出错、

因此引出了泛型的泛型中的类型在使用时指定,不需要强制类型转换的好处。

二:泛型类,泛型接口,泛型方法

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?

顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等),并且用<>括起来,并放在类名的后面。泛型类是允许有多个类型变量的。

以下为几个例子

public class Generic{
private T data;
public T getData() {
    return data;
}
public void setData(T data) {
    this.data = data;
}
}

public class Generic1{
 private T data;
 private K result;
 }

public interface Callback{
 public T result();
}

泛型接口与泛型类的定义基本相同,实现泛型接口的类有两种实现方法
1.直接在使用的时候指定具体类型 ,实现类new出来的与普通的类没区别

public class ImplGenerator implements Callback{
     @Override
     public String result(String data) {
            return "OK";
     }
}

2.未传入泛型参数时,在实现类中实现接口时与实现类的泛型类型保持一致,实现类在new出来时需要指定类型

public class ImplGenerator1 implements Callback{
    @Override
    public T result(T data) {
        return data;
     }
}

//指定类型
ImplGenerator1 stringImplGenerator1 = new ImplGenerator1<>();


泛型方法
     是在调用方法的时候明确指定泛型的具体类型,泛型方法可以在任何地方和场景使用,包括普通类和泛型类,注意泛型类型中定义的普通方法和泛型方法的区别:

1.普通方法

public class Generic{
    private T data;
    public T getData() {   
         return data;
    }
    public void setData(T data) {   
         this.data = data;
        }
}

        此时虽然在getData方法中使用了泛型,但是他并不是泛型方法,这个只是一个普通的成员方法,只不过他是在声明泛型类的时候指定的泛型类型,所以他只是使用了泛型类型T而已。

2.泛型方法

public T showData(Generic tGeneric){
    T data = tGeneric.getData();
     return data;
}

        这才是一个真正的泛型方法,首先泛型方法必须要有表示是一个泛型方法,并且声明了一个泛型T,这个T 可以出现在这个泛型方法的任意位置,泛型的数量也可以是多个。如:public T showData(Generic tGeneric){...}

三:如何限定类型变量
有些时候,我们在写一写泛型方法的时候不可避免的会对要泛型的类型做出限定,例如如果我们要计算两个变量的大小

public T min(T a,T b){
     if (a.compareTo(b) >0 ) return b;else return a;
}

那么如果来保证传入的泛型类型都带有compareTo这个方法呢?这个时候就要做出限定了,让T一定带有compareTo方法

public T min(T a,T b){
 if (a.compareTo(b) >0 ) return b;else return a;
}

        T extends Comparable 中 T表示了绑定类型的子类型,Comparable表示了绑定类型,子类型和绑定类型可以是类也可以是接口,如果这个时候传入一个没有实现接口Comparable的类的实例,将会发生编译错误。

同时extends左右都允许有多个,如 T,V extends Comparable&Serializable注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限列表的第一个。这种类的限定既可以用在泛型方法上也可以用在泛型类上。

四:泛型中的约束和局限性

1.不能用基本类型实例化类型参数

//这种不被允许
Generic generic = new Generic();

2.运行时类型查询只适用于原始类型

Generic generic1 = new Generic();
Generic generic2 = new Generic();
//以下两种不被允许
// if(generic1 instanceof Generic)
// if(generic1 instanceof Generic)
System.out.println(generic1.getClass() == generic2.getClass());
System.out.println(generic1.getClass().getName());
//打印结果
true
com.company.Main$Generic

证明了 获取到的类型只是原始的类型Generic 所以结果为ture

3.泛型类的静态上下文中类型变量失效

//静态域活方法里不能引用类型变量
private static T instance;
//静态方法 本身是泛型方法就行
public static T getInstance(){}

        不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。

五:泛型类型的继承规则

public class Father {}
public class Son extends Father {}
public class Pair {
    private T one; private T two;
    public T getOne() { return one; }
    public void setOne(T one) { this.one = one; }
    public T getTwo() { return two; }
    public void setTwo(T two) { this.two = two; }
}

//Pair与Pair没有任何关系 完全单独独立
Pair fatherPair = new Pair<>();
Pair son = new Pair<>();
//证明一下 正常情况下 父类可以创建出子类
Father father = new Son();
//会报错 无法创建显示类型不一样
Pair fatherPair1 = new Pair();

  但是泛型类可以继承或者扩展其他泛型类,比如List和ArrayList

Pair pair =new ExtendsPair<>();

六:通配符类型

上边已经说过了Pair与Pair没有任何关系 完全单独独立,如果我们有一个泛型类和一个方法呢

public class Fruit {
    private String color;
    public String getColor() { return color; }
    public void setColor(String color) { this.color = color; }
}

private class Apple extends Fruit {}
private class Orange extends Fruit {}
private class youzi extends Orange {}
public class GenericType {
 private T data;
 public T getData() { return data; }
 public void setData(T data) {
 this.data = data;}
}

public void use() {
 GenericType type = new GenericType<>();
 print(type);
 GenericType type1 = new GenericType<>();
//这样是不被容许的
 print(type1);
}

为解决这个问题,于是提出了一个通配符类型 ?

有两种使用方式:

?extends X  表示类型的上界,类型参数是X的子类

?super X  表示类型的下界,类型参数是X的超类

这两种方式从名字上来看,特别是super,很有迷惑性,下面我们来仔细辨析这两种方法。

?extends X
表示传递给方法的参数,必须是X的子类(包括X本身)

public void print2(
GenericType P) {
 System.out.print(P.getData().getColor());
}
GenericType a= new GenericType<>();
print2(a);

但是对泛型类GenericType来说,如果其中提供了get和set类型参数变量的方法的话,set方法是不允许被调用的,会出现编译错误

GenericType type1 = new GenericType<>();
Fruit fruit = new Fruit();
Apple apple = new Apple();
//下面的set是不被容许的
type1.setData(fruit);
type1.setData(apple);

get方法则没问题,会返回一个Fruit类型的值。

GenericType type = new GenericType<>();
Fruit data1 = type.getData();

为何?

道理很简单,?extends X  表示类型的上界,类型参数是X的子类,那么可以肯定的说,get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是X的那个子类,不知道。

总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。

?super X
表示传递给方法的参数,必须是X的超类(包括X本身)

private void printSuper(GenericType o) {
    System.out.print(o.getData());
}
private void printUse() {
    GenericType type = new GenericType<>();
    GenericType type1 = new GenericType<>();
    GenericType type2 = new GenericType<>();
    GenericType type3 = new GenericType<>();
    printSuper(type);
    printSuper(type2);
    //下边两个会报错
    printSuper(type1);
    printSuper(type3);
}

private void printSuper(GenericType o) {
 System.out.print(o.getData());
}

但是对泛型类GenericType来说,如果其中提供了get和set类型参数变量的方法的话,set方法可以被调用的,且能传入的参数只能是X或者X的子类

GenericType genericType = new GenericType<>();    
//下边的 apple 和 fruit 会报错   
 genericType.setData(new Apple());    
 genericType.setData(new Fruit());    
//这两个不会报错   
 genericType.setData(new Orange());   
 genericType.setData(new youzi()); 

//一定是OBJECT
Object data = genericType.getData();

get方法只会返回一个Object类型的值

为何?

?super  X  表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。

总结:主要用于安全地写入数据,可以写入X及其子类型。

虚拟机是如何实现泛型的?

        泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。,由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。
        泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<integer>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
        Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<integer>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
        将一段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型

method

        上面这段代码是不能被编译的,因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。
        由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。
        另外,从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

你可能感兴趣的:(java中泛型的理解)