java 泛型

一、为什么有泛型

 

早期的Object类型可以接收任意的对象类型,但是在实际的使用中,会有类型转换的问题。也就存在这隐患,所以Java提供了泛型来解决这个安全问题。

ArrayList names = new ArrayList();
names.add("hello");
names.add(123);   //无限制,可以插入
for (int i = 0; i < names.size(); i++) {
        //需求:打印每个字符串的长度,就要把对象转成String类型
        String str = (String) names.get(i);
        System.out.println(str.length());
 }

运行这段代码,程序在运行时发生了异常:因为names里插入了Integer类型数据,在转成String时出错。

我们期望在编译时在names.add(123)就被告知错误,而不是等到运行到String str = (String) names.get(i);

在jdk1.5后,泛型应运而生。让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

 

ArrayList<String> names = new ArrayList<>();
names.add("hello");
names.add(123); //编译时报错,123无法插入

 

二、什么是泛型

泛型:是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在类、方法和接口中,分别被称为泛型类泛型方法泛型接口

注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

 

三、泛型的优点

  • 避免了类型强转的麻烦。
  • 它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
  • 可读性,从字面上就可以判断集合中的内容 类型

四、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型方法、泛型接口。将数据类型作为参数进行传递。

4.1 泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种集合框架容器类,如:List、Set、Map。

  • 泛型类的定义格式:

修饰符 class 类名<代表泛型的变量}

public class GenericsClass<T> {
    //t这个成员变量的类型为T,T可以随便写为任意标识,常见的有T、E、K、V等形式,T的类型由外部指定
    private T t;
    //泛型构造方法形参t的类型也为T,T的类型由外部指定
    public GenericsClass(T t) {
        this.t = t;
    }
    //泛型方法getT的返回值类型为T,T的类型由外部指定
    public T getT() {
        return t;
    }
}

 

泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。即:在创建对象的时候确定泛型。

GenericClass<String> genericString = new GenericClass<String>("GenericsString");
GenericClass<Integer> genericInteger = new GenericClass<Integer>(123);

使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型会起到本应起到的限制作用。

如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

即跟之前的案例一样,没有写ArrayList的泛型类型,容易出现类型强转的问题。

 

4.2 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中。

  • 定义格式

  修饰符 interface接口<代表泛型的变量}

 

public interface GenericsInteface<T> {
    public abstract void add(T t); 
}

使用格式

  • 1、定义类时确定泛型的类型
public class GenericsImp implements GenericsInteface<String> {
    @Override
    public void add(String s) {
        System.out.println("设置了泛型为String类型");
    }
}
  • 2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class GenericsImp<T> implements GenericsInteface<T> {
    @Override
    public void add(T t) {
        System.out.println("没有设置类型");
    }
}
public class GenericsTest {
    public static void main(String[] args) {
        GenericsImp<Integer> gi = new GenericsImp<>();
        gi.add(123);
    }
}

 

4.3 泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 。

  • 定义格式:

修饰符 <代表泛型的变量> 返回值类型 方法名(参数)}

 

public <T> T genercMethod(T t){
        System.out.println(t.getClass());
        System.out.println(t);
        return t;
    }
public static void main(String[] args) {
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("helloGeneric"); //这里的泛型跟下面调用的泛型方法可以不一样。
    String str = genericString.genercMethod("hello");//传入的是String类型,返回的也是String类型
    Integer i = genericString.genercMethod(123);//传入的是Integer类型,返回的也是Integer类型
}

1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。

2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数

 习惯了类型参数放在类的后面,如ArrayList<String>,泛型方法为什么不放在后面?看一个例子:

public static <T,S> T f(T t){return t;}
public static class a{}
public static class b{}

@Test
public void test(){
  a c=new a();
    <a,b>f(c);//OK
  f<a,b>(c);//error,看起来像是一个逗号运算符连接的两个逻辑表达式,当然目前java中除了for(...)并不支持逗号运算符
}

如果泛型方法定义在泛型类中,而且类型参数一样:
public class GenericMethod<T> {
    public <T> void sayHi(T t){
        System.out.println("Hi "+t);
    }
}
是不是说,定义GenericMethod时传了 Integer 类型,sayHi()也就自动变成 Integer 了呢?No。
String i="abc";
new GenericMethod<Integer>().<String>sayHi(i);

该代码运行一点问题都没有。原因就在于泛型方法中的<T>,如果去掉它,就有问题了。


类型参数的限定

如果限制只有特定某些类可以传入T参数,那么可以对T进行限定,如:只有实现了特定接口的类:<T extends Comparable>,表示的是Comparable及其子类型。

为什么是extends不是 implements,或者其他限定符?

严格来讲,该表达式意味着:`T subtypeOf Comparable`,jdk不希望再引入一个新的关键词;

其次,T既可以是类对象也可以是接口,如果是类对象应该是`extends`,而如果是接口,则应该是`implements`;从子类型上来讲,extends更接近要表达的意思。 好吧,这是一个约定。

限定符可以指定多个类型参数,分隔符是 &,不是逗号,因为在类型参数定义中,逗号已经作为多个类型参数的分隔符了,如:<T,S extends Comparable & Serializable>

泛型限定的优点:

限制某些类型的子类型可以传入,在一定程度上保证类型安全;

可以使用限定类型的方法。

注:

我们知道final类不可继承,在继承机制上class SomeString extends String是错误的,但泛型限定符使用时是可以的:<T extends String>,只是会给一个警告。

 

五、泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

ArrayList<?> list1 = new ArrayList<Object>();
ArrayList<?> list2 = new ArrayList<String>();
ArrayList<?> list3 = new ArrayList<Integer>();

泛型中可以指定一个泛型的上限下限

泛型的上限:

  • 格式 类型名称 <? extends 类 > 对象名称
  • 意义: 只能接收该类型及其子类

泛型的下限:

  • 格式: 类型名称 <? super 类 > 对象名称
  • 意义: 只能接收该类型及其父类型

 

六、泛型擦除

java引入泛型时,为了保持代码的向后兼容性,实际上引入的是伪泛型,

ArrayList<String> list1 = new ArrayList<String>(); 
list1.add("abc"); 
ArrayList<Integer> list2 = new ArrayList<Integer>(); 
list2.add(123); 
System.out.println(list1.getClass() == list2.getClass());

输出结果是true

这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。

Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程就是泛型擦除。

 

在泛型代码内部,无法获取有关泛型参数类型的信息

List<String> list = new ArrayList<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));

输出结果是

[E]

你只能看到用作参数占位符的标识符,这并非有用的信息。Java 泛型是使用擦除实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。

因此 List<String> 和 List 在运行时实际上是相同的类型,它们都被擦除成原生类型 List

 

但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。通过javap命令 可以看到在Constant pool中#5 Signature记录了泛型的类型;使用侧泛型则不会。

signature:泛型类中独有的标记,普通类中没有,JDK5才加入,标记了定义时的成员签名,包括定义时的泛型参数列表,参数类型,返回值等;

声明侧泛型主要指以下内容

 1.泛型类,或泛型接口的声明
2.带有泛型参数的方法
3.带有泛型参数的成员变量

使用侧泛型

也就是方法的局部变量,方法调用时传入的变量。

 

例如retrofit是通过getGenericReturnType来获取类型信息的。

jdk的Class 、Method 、Field 类提供了一系列获取 泛型类型的相关方法。

以Method为例,getGenericReturnType获取带泛型信息的返回类型 、 getGenericParameterTypes获取带泛型信息的参数类型

 
 Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。
也就是说java的class文件会保存继承的父类或者接口的泛型信息。
 
 public List<String> parse(String jsonStr){
      List<String> topNews = new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
       return topNews;
 }

上面这段代码

1.Gson是怎么获取泛型类型的,也是通过Signature吗?

2.为什么Gson解析要传入匿名内部类?这看起来有些奇怪

 

Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析
所以Gson使用了一个巧妙的方法来获取泛型类型:
 
1.创建一个泛型抽象类TypeToken,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类。
2.创建一个 继承自TypeToken的匿名内部类, 并实例化泛型参数TypeToken<String>
3.通过class类的public Type getGenericSuperclass()方法,获取带泛型信息的父类Type,也就是TypeToken<String>
总结:Gson利用子类会保存父类class的泛型参数信息的特点。通过匿名内部类实现了泛型参数的传递。
 
七、什么是PECS原则?
7.1 PECS介绍
PECS的意思是Producer Extend Consumer Super,简单理解为如果是生产者则使用Extend,如果是消费者则使用Super,不过,这到底是啥意思呢?
PECS是从集合的角度出发的
1.如果你只是从集合中取数据,那么它是个生产者,你应该用extend
2.如果你只是往集合中加数据,那么它是个消费者,你应该用super
3.如果你往集合中既存又取,那么你不应该用extend或者super
让我们通过一个典型的例子理解一下到底什么是Producer和Consumer
 
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)); 
          } 
    } 
}

上面的例子中将src中的数据复制到dest中,这里src就是生产者,它「生产」数据,dest是消费者,它「消费」数据。

7.2 为什么需要PECS

使用PECS主要是为了实现集合的多态举个例子,现在有这样一个需求,将水果篮子中所有水果拿出来(即取出集合所有元素并进行操作)

public static void getOutFruits(List<Fruit> basket){
    for (Fruit fruit : basket) { 
        System.out.println(fruit);//...do something other 
     }
}
List<Fruit> fruitBasket = new ArrayList<Fruit>();
getOutFruits(fruitBasket);//成功
List<Apple> appleBasket = new ArrayList<Apple>();
getOutFruits(appleBasket); //编译错误

 

1.将List<Apple>传递给List<Fruit>会编译错误。

2.因为虽然Fruit是Apple的父类,但是List<Apple>和List<Fruit>之间没有继承关系

3.因为这种限制,我们不能很好的完成取出水果篮子中的所有水果需求,总不能每个类型都写一遍一样的代码吧?

使用extend可以方便地解决这个问题

/**参数使用List<? extends Fruit>**/
public static void getOutFruits(List<? extends Fruit> basket){
    for (Fruit fruit : basket) { 
        System.out.println(fruit);//...do something other
    }
}
public static void main(String[] args){
     List<Fruit> fruitBasket = new ArrayList<>(); 
     fruitBasket.add(new Fruit()); 
     getOutFruits(fruitBasket); 
     List<Apple> appleBasket = new ArrayList<>(); 
     appleBasket.add(new Apple()); 
     getOutFruits(appleBasket);//编译正确}

List<? extends Fruit>,同时兼容了List<Fruit>和List<Apple>,我们可以理解为List<? extends Fruit>现在是List<Fruit>和List<Apple>的超类型(父类型)

通过这种方式就实现了泛型集合的多态

 在List<? extends Fruit>的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit,具体是Fruit的哪个子类是无法知道的。所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit,所以从这个泛型集合中取Fruit类型的元素是可以的。
 
在List<? super Apple>的泛型集合中,元素的类型是Apple的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。插入元素时可以插入Apple与Apple的子类,因为这个集合中的元素都是Apple的父类,子类型是可以赋值给父类型的。
 
有一个比较好记的口诀:
1.只读不可写时,使用List<? extends Fruit>:Producer
2.只写不可读时,使用List<? super Apple>:Consumer
 
 总得来说,List<Fruit>和List<Apple>之间没有任何继承关系。API的参数想要同时兼容2者,则只能使用PECS原则。这样做提升了API的灵活性,实现了泛型集合的多态,当然,为了提升了灵活性,自然牺牲了部分功能。鱼和熊掌不能兼得。
 
八、注意问题

8.1 任何基本类型不能用作类型参数

Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList<int> 之类的东西。 解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换,这几乎就像是有一个 ArrayList<int> 一样

 

8.2 实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:

interface Payable<T>{};
class Employe implement Payable <Employe>{};

class Hourly extends Employe implement Payable <Hourly>{};

 

Hourly 不能编译,因为擦除会将 Payable<Employe> 和 Payable<Hourly> 简化为相同的类 Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 Payable 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译

 

8.3 转型

使用带有泛型类型参数的转型不会有任何效果,由于擦除的原因,编译器无法知道这个转型是否是安全的,而且T 被擦除到它的第一个边界,默认情况下是 Object,因此实际上只是转型为 Object

 

8.4 重载

下面的程序是不能编译的,因为擦除,所以重载方法产生了相同的类型签名

public class UseList<T,W>{
      void f(List<T> a);
      void f(List<W> b);
}

 

8.5 类型检查不可使用泛型

if(aaa instanceof Pair<String>){}//error

Pair<String> p = (Pair<String>) a;//warn

 

8.7 不能创建泛型对象数组

 

GenericMethod<User>[] o=null;//ok
o=new GenericMethod<User>[10];//error

 

 

可以定义泛型类对象的数组变量,不能创建及初始化。

注,可以创建通配类型数组,然后进行强制类型转换。不过这是类型不安全的。

o=(GenericMethod<User>[]) new GenericMethod<?>[10];

 

不可以创建的原因是:因为类型擦除的原因无法在为元素赋值时类型检查,因此jdk强制不允许。

有一个特例是方法的可变参数,虽然本质上是数组,却可以使用泛型。

安全的方法是使用List。

public static <T> T getMiddle(T... a){
  return a[a.length/2];
}

 

8.8  不能实例化泛型对象

T t= new T();//error
T.class.newInstance();//error
T.class;//error

 

解决办法是传入Class<T> t参数,调用t.newInstance()

public void sayHi(Class<T> c){
  T t=null;
  try {
    t=c.newInstance();
  } catch (Exception e) {
    e.printStackTrace();
  }
  System.out.println("Hi "+t);
}

 

8.9  不能在泛型类的静态域中使用泛型类型

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

8.10 不能捕获泛型类型的对象

Throwable类不可以被继承,自然也不可能被catch

public class GenericThrowable<T> extends Throwable{
  //The generic class GenericThrowable<T> may not subclass java.lang.Throwable
}

 

但由于Throwable可以用在泛型类型参数中,因此可以变相的捕获泛型的Throwable对象。

@Test
public void testGenericThrowable(){
  GenericThrowable<RuntimeException> obj=new GenericThrowable<RuntimeException>();
  obj.doWork(new RuntimeException("why?"));
}

public static class GenericThrowable<T extends Throwable>{
  public void doWork(T t) throws T{
    try{
      int i=3/0;
    }catch(Throwable cause){
      t.initCause(cause);
      throw t;
    }
  }
}

 

但会有如下情形

@Test
public void testGenericThrowable(){
  GenericThrowable<RuntimeException> obj=new GenericThrowable<RuntimeException>();
  obj.doWork(new RuntimeException("What did you do?"));
}
public static class GenericThrowable<T extends Throwable>{
  public void doWork(T t) throws T{
    try{
      Reader reader=new FileReader("notfound.txt");
      //这里应该是checked异常
    }catch(Throwable cause){
      t.initCause(cause);
      throw t;
    }
  }
}
FileReader实例化可能抛出已检查异常,jdk中要求必须捕获或者抛出已检查异常。这种模式把它给隐藏了。也就是说可以消除已检查异常,有点不地道,颠覆了java异常处理的认知,后果不可预料,慎用。

擦除的冲突

重载与重写

定义一个普通的父类:

package com.pollyduan.generic;

public class Parent{

    public void setName(Object name) {
        System.out.println("Parent:" + name);
    }
}

那么继承一个子类,Son.java

package com.pollyduan.generic;

public class Son extends Parent {
    public void setName(String name) {
        System.out.println("son:" + name);
    }

    public static void main(String[] args) {
        Son son=new Son();
        son.setName("abc");
        son.setName(new Object());
    }
}

Son类重载了一个setName(String)方法,这没问题。输出:

son:abc
Parent:java.lang.Object@6d06d69c

Parent修改泛型类:

package com.pollyduan.generic;

public class Parent<T>{

    public void setName(T name) {
        System.out.println("Parent:" + name);
    }
}

从擦除的机制得知,擦除后的class文件为:

package com.pollyduan.generic;

public class Parent{

    public void setName(Object name) {
        System.out.println("Parent:" + name);
    }
}

这和最初的非泛型类是一样的,那么Son类修改为:

package com.pollyduan.generic;

public class Son extends Parent<String>  {
    public void setName(String name) {
        System.out.println("son:" + name);
    }

    public static void main(String[] args) {
        Son son=new Son();
        son.setName("abc");
        son.setName(new Object());//The method setName(String) in the type Son is not applicable for the arguments (Object)
    }
}

发现重载无效了。这是泛型擦除造成的,无论是否在setName(String)是否标注为@Override都将是重写,都不是重载。而且,即便你不写setName(String)方法,编译器已经默认重写了这个方法。

换一个角度来考虑,定义Son时,Parent已经明确了类型参数为String,那么再写setName(Stirng)是重写,也是合理的。

package com.pollyduan.generic;

public class Son extends Parent<String>  {

    public static void main(String[] args) {
        Son son=new Son();
        son.setName("abc");//ok
    }
}

反编译会发现,编译器在内部编译了两个方法:

  public void setName(java.lang.String);
  public void setName(java.lang.Object);

setName(java.lang.Object) 虽然是public但编码时会发现不可见,它称为"桥方法",它会重写父类的方法。

Son son=new Son();
Parent p=son;
p.setName(new Object());

强行调用会转换异常,也就证明了它实际上调用的是son的setName(String)。

我非要重载怎么办?只能曲线救国,改个名字吧。

public void setName2(String name) {
        System.out.println("son:" + name);
    }

继承泛型的参数化

一个泛型类的类型参数不同,称之为泛型的不同参数化。

泛型有一个原则:一个类或类型变量不可成为两个不同参数化的接口类型的子类型。如:

package com.pollyduan.generic;

import java.util.Comparator;

public class Parent implements Comparator{

    @Override
    public int compare(Object o1, Object o2) {
        return 0;
    }
}

public class Son extends Parent  implements Comparator   {
}

这样是没有问题的。如果增加了泛型参数化:

package com.pollyduan.generic;

import java.util.Comparator;

public class Parent implements Comparator<Parent>{

    @Override
    public int compare(Parent o1, Parent o2) {
        return 0;
    }
}

package com.pollyduan.generic;

import java.util.ArrayList;
import java.util.Comparator;

public class Son extends Parent  implements Comparator<Son>   {
  //The interface Comparator cannot be implemented more than once with different arguments
}

原因是Son实现了两次Comparator<T>,擦除后均为Comparator<Object>,造成了冲突。

 

参考链接
https://zhuanlan.zhihu.com/p/337324037
https://baijiahao.baidu.com/s?id=1705955740008713860&wfr=spider&for=pc
https://www.jb51.net/article/216499.htm
 https://www.imooc.com/article/18159
 
 

 

 

 

posted @ 2022-06-09 15:45  diameter  阅读(134)  评论(0编辑  收藏  举报