美文网首页
Java基础-泛型

Java基础-泛型

作者: czn5991 | 来源:发表于2019-04-09 14:59 被阅读0次

什么是泛型

泛型程序设计就是为了让一段代码能够被很多不同类型的对象所重用。Java提供的ArrayList就使用了泛型,使得该类能够存储多种不同的类型对象,比如ArrayList<String>保存String对象、ArrayList<Integer>保存Integer对象。这里<String><Integer>中被尖括号包起来的就是类型参数,类型参数表明了该泛型的实例对象中元素的类型。

使用泛型

我们可以定义泛型类和泛型方法,下面是两个例子。

泛型类

泛型类就是具有一个或多个泛型变量的类。下面是一个泛型类的例子。例子中T即为泛型变量,泛型变量使用的是大写形式,一般有E表示集合的元素类型,KV表示键值对的类型、TUS表示任意类型。使用了泛型变量后,就可以在类定义中使用T来代表方法的参数、方法的返回类型和变量的类型。

public class Pair<T>{
  private T first;
  private T second;
  public void setFirst(T first){...}
  public T getFirst(){...}
}
泛型方法

我们可以不定义泛型类,而在一个具体的类定义中使用泛型方法。下面是一个泛型方法的例子。泛型方法的泛型变量放置在修饰符的后面,返回类型的前面。在调用泛型方法的时候可以显示的说明具体类型,也可以在有足够信息让编译器推断出类型的时候省略类型参数(当然,如果信息不够就会抛出异常)。

class ArrayAlg{
  public static <T> T getMiddle(T... a){}
}

String middle=ArrayAlg.<String>getMiddle("A","B","C");
泛型变量

在定义泛型类或泛型方法的时候,可以使用的泛型变量不仅仅上上面所提到的TE等,还可以通过extends关键词来限定,比如使用<T extends Comparable>来表明TComparable这个限定类型的子类型,或者可以粗略的理解为其派生类,不过这个例子里并不是说子类型必须是限定类型的派生类,也可以是实现了限定类型接口的子类型
当限定类型有多个的时候,可以使用&来进行连接,比如<T extends Comparable & Seriablizable>

虚拟机中的泛型代码

虚拟机没有泛型类型对象,所有对象都属于普通类,因此泛型在编译后会被替换为原始类型

类型擦除

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。在定义一个泛型类型的时候,都自动提供了一个相应的原始类型,比如在泛型类和泛型方法的例子中原始类型为Object,在使用了限定类型的时候原始类型就为限定类型。这个过程就是擦除类型变量,将其替换为限定类型。对于上面的ArrayAlg被擦除后的原始类型如下。

public class ArrayAlg{
  public statac Object getMiddle(Object... a){}
}

在类型擦除后,在调用的时候将会发生类型转换。比如String middle=ArrayAlg.<String>getMiddle("A","B","C");,在实际执行的时候会先调用原始类型的getMiddle()然后再将Object转换为String类型返回。

在对泛型进行类型擦除后,可能会出现两个复杂的问题。

  1. 两个Setter
    上述的Pair<T>被擦除后,其setter方法将会变为public void setFirst(Object first)。当有一个类继承了Pair<LocalDate>类并重写了setFirst()的时候,将会出现两个setFirst()方法(参数类型不同)。
public class DateInterval extends Pair<LocalData>{
  public void setFirst(LocalData first){...}
}

//被擦除后的结果
public class DateInterval extends Pair{
  //派生类中实现的
  public void setFirst(LocalData first){...}
  //Pair中继承的
  public void setFirst(Object first){...}
}

该类被擦除后的代码如上,此时Pair中原来的setFirst(T first)就变成了setFirst(Object first),与DateInterval中定义的setter方法参数不同,因此DateInterval中就出现了两个setFirst方法。
为了解决这个问题,我们可以让编译器自行决定使用最适合的方法,或者使用一个桥方法重写继承的setter

public void setFirst(Object first){
  setFirst((LocalData) first);
}
  1. 两个getter
    两个getter方法与上面的问题相似,当派生类重写了getFirst()就会出现两个getter方法(返回类型不同),一个返回Object,一个返回LocalData,这个在Java编码中是不允许的。这个问题我们不用过多操心,虚拟机能够自行处理这个问题。
调用遗留代码

假设我们实现了一个JSlider类,通过setLabelTable(Dictionary table)来设置JSlider标签,这里Dictionary是原始类型。如果我们通过Dictionary<Integer,Component>实例化了一个泛型类Dictionary,再调用setter方法,编译器将会进行警告,因为编译器无法确定setter方法会对Dictionary对象做什么操作,可能会将Integer进行替换,使得关键字不再是Integer。这种情况下,我们可以通过@SuppressWarnings("unchecked")进行标注,关闭对方法中代码的检查。

泛型的约束与限制

在使用泛型的时候,有很多限制需要注意,其中大多数都是由类型擦除引起的。

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

没有Pair<int>,只有Pair<Integer>,这是因为Pair类含有Object类型的域,而Object不能存储int值。

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

在虚拟机中泛型被擦除为原始类型,因此类型查询只返回原始类型。

  • if(a instanceof Pair<String>)//error实际上只测试a是否为任意类型的一个Pair
  • getClass也总是返回原始类型,if(stringPair.getClass()==employeePair.getClass())// true
  • 在强制类型转换时,也会抛出错误,Pair<String> p=(Pair<String>)a;//Warning-can only test that a is a Pair
不能创建参数化类型的数组

不能实例化参数化类型的数组,例如Pair<String>[] table=new Pair<String>[10];//error。在这种情况下,擦除之后table的类型实际为Pair[],程序员可以将其转换为Object[],即Object[] objArray= table;。这样转换后,如果试图存储除了Pair的其他类型如objArray[0]="string";,将会得到错误Error-component type is Pair。因此,无法差un关键参数化类型的数组。
我们可以声明通配类型的数组,然后进行类型转换来通过编译,Pair<String>[] table=(Pair<String>[])new Pair<?>[10];。这种方法是不安全的,如果往table中存储Pair<Employee>是可以成功的,但是对table[0].getFirst()调用一个String将会报出错误。

Varargs警告

在调用一个参数个数可变的函数时,如public static <T> void addAll(Collection<T> coll,T... ts)ts实际上是一个数组,包含所有的实参。上面我们提到过,无法创建参数化类型的数组,在这里Java虚拟机必须创建一个Pair<T>数组。对于这种情况,我们在调用的时候只会得到一个警告,我们可以通过@SupressWarning("unchecked")@SafeVarargs标注关闭方法中代码的检测。

不能实例化类型变量

在泛型类中或泛型方法中,不能使用像new T(...)T.class这样的表达式来实例化类型变量,因为类型变量被擦除后会变成Object对象,而本意是不希望掉用new Object()的。下面这个例子就是非法的。

public class Pair<T>{
  public Pair(){
    first=new T();
  }
}

解决办法是让调用者提供一个构造器的表达方式。

public static <T> Pair<T> makePair(Supplier<T> constr){
  return new Pair<>(constr.get(),constr.get());
}

Pair<String> p=Pair.makePair(String::new);
不能构造泛型数组

在泛型类或方法中不能构造泛型数组的原因是类型擦除会使得永远创建相同类型的数组。比如T[] array=new T[2],实际上会永远创建原始类型的数组。
如果创建该数组只是为了作为一个私有变量操作,那么可以创建Object数组,然后在调用的时候再进行类型转换。

public class ArrayList<E>{
  private Object[] elements;
  public ArrayList(){
    elements=(E[]) new Object[10];
  }
  public E get(int n){
    return (E) elements[n];
  }
}

这种方法不是万能的,当限定类型不能通过Object强制转换得到时,将会发生ClassCastException异常。这时候的解决办法就是提供一个数组构造器表达式。

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr,T... a){
  T[] mm=constr.apply(2);
  ...
}

String[] ss=ArrayAlg.minmax(String[]::new,"A","B","C");
泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

public class Singletion<T>{
  private static T singletonIns;//error
  public static T getSingleIns(){//error
    if(singletonIns==null) construct one;
    return singletonIns;
  }
}
不能抛出或捕获泛型类的实例

不能抛出也不能捕获泛型类对象,泛型类扩展Throwable也是不合法的。

public static <T extends Throwable> void doWork(Class<T> t){
  try{}
  catch(T e){}//error-cant catch type variable
}

不过,在异常规范中使用类型变量是允许的。(?)

public static <T extends Throwable> void doWork(T t) throws T{
  try{}
  catch(Throwable realCause){
    t.initCause(realCause);
    throw t;


  }
}
可以消除对受查异常的检查
注意擦除后的冲突

当泛型类型擦除时,可能会导致一些冲突。

  1. 方法冲突
    在下面这个例子中,Pair<T>类中实现了euqals(T value)方法,在类型擦除后为euqals(Object value)方法,与从Object继承的euqals(Object value)方法同名冲突。
public Pair<T>{
  public boolean euqals(T value){
    //Name clash:The method equals(T) of type Generics<T> has the same erasure as equals(Object) of type Object but does not override it
    return first.equals(value)&&second.euqals(value);
  }
}
  1. 类冲突
    泛型规范说明中有提到:“想要支持擦除的转换,就需要强行限制一个类或类型变量不能同成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。”例如下面的代码就是非法的。
class Employee implements Comparable<Employee>{}
class Manager extends Employee implements Comparable<Manager>{}//error

泛型类型的继承规则

考虑一个类和其子类EmployeeManagerPair<Employee>却与Pair<Manager>没有任何关系,其之间没有继承的关系。也就是说无论ST有什么联系,Pair<S>Pair<T>都没有什么联系。

泛型_继承规则.png

通配符类型

上面说明了无论ST有什么联系,Pair<S>Pair<T>都没有什么联系。但是我们在实际使用的时候,并不希望Pair<Employee>不能够存储Pair<Manager>,否则要分类存储,变向地增加了代码的繁琐程度。Java中提供了通配符来解决这个问题。

通配符概念

首先要分清楚通配符与类型变量的区别。类型变量用于在定义泛型类或泛型方法的时候使用,而通配符是在使用泛型的时候来对泛型起限制作用。
使用了通配符后,允许泛型实例的变量类型发生变化,比如Pair<? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类,不再仅仅局限于Employee。这样这个Pair就既可以存储Manager又可以存储Employee了。
但是这里存在一个细节问题,对于下面这个例子,setter方法会出现编译错误,而getter方法不会。这是因为setter方法只说明了参数是Employee的派生类,但是不知道具体是什么类型,但是getter方法知道返回类型是Employee的派生类,可以将其派生类复制给Employee的引用。

Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies=managerBuddies;//ok
wildcardBuddies.setFirst(lowlyEmployee);//compile-time error

? extends Employee getFirst();
void setFirst(? extends Employee);//error
通配符超类型限定

除了上述的<? extends Employee>表示Employee的派生类类型参数外,还可以通过<? supper Employee>表示Employee的超类类型参数。
同样,使用了超类类型限定后,无法使用getter方法而可以使用setter方法。

? supperEmployee getFirst();//error
void setFirst(? supperEmployee);
无限定通配符

可以使用无限定的通配符,Pair<?>来表示存储任何类型。这个和原始类型的Pair类型有较大的区别,使用该通配符后无法再使用setter方法。
无限定通配符通常被用来进行简单的操作,比如下面的判断是否为空引用。

public static boolean hasNulls(Pair<?> P){
  return p.getFirst()==null || p.getSecond()==null;
}
通配符捕获

对于一个交换元素的方法public static void swap(Pair<?> p),我们无法在方法中使用? t=p.getFirst()来初始化变量,也就是说无法使用?来表示类型。在这种情况下,我们需要通过一个辅助方法来解决。

public static <T> void swapHelper(Pair<T> p){
  T t=p.getFirst();
  p.setFirst(p.getSecond());
  p.setSecond(t);
}

public static void swap(Pair<?> p){swapHelper(p);}

当然,这里也可以直接使用泛型来解决这个问题。但是在想要使用supper关键字而不得不使用通配符的时候,该方法就是必须了。

public static void maxminBonus(Manager[] a,Pair<? supper Manager> result){
  minmaxBonus(a,result);
  PairAlg.swap(result);//这里就需要一个swapHelper
}                                                                                

反射和泛型

泛型由于会发生类型擦除,因此在使用反射的时候得不到太多信息,下面将介绍用反射能够得到的信息。

泛型Class类

Class类是泛型的,String.class实际上是一个Class<String>类的对象。下面是Class<T>中使用了类型参数的方法。

T newInstance();
T cast(Object obj);
T[] getEnumConstants();
Class<? super T> getSuperclass();
Constructor<T> getConstructor(Class... parameterTypes);
Constructor<T> getDeclaredConstructor(Class... parameterTypes);
使用Class<T>参数进行类型匹配
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,IllegalAccessException{
  return new Pair<>(c.newInsatance(),c.newInstance());
}
虚拟机中的泛型类型信息

虚拟机中被擦除的类仍能够保持一些泛型祖先的信息。对于方法public static Comparable min(Comparable[] a),这是一个泛型方法public static <T extends Comparable<? super T>> T min(T[] a)的擦除。我们可以通过反射API来确定:

  • 这个泛型方法有一个叫做T的类型参数
  • 这个类型参数有一个子类型限定,其自身又是一个泛型类型
  • 这个限定类型有一个通配符参数
  • 这个通配符参数有一个超类型限定
  • 这个泛型方法有一个泛型数组参数
    java.lang.reflect包中提供了Type接口,包含下列子类型:
  • Class类,描述具体类型
  • TypeVariable接口,描述类型变量(如T extends Comparable<? super T>
  • WildcardType接口,描述通配符(如? super T
  • ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>
  • GenericArrayType接口,描述泛型数组(如T[]

相关文章

  • Java 泛型

    导读 移动开发知识体系总章(Java基础、Android、Flutter) 为什么会有泛型? 泛型类 泛型方法 泛...

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • Java基础之泛型

    Java基础之泛型 泛型基本介绍 简单介绍在jdk1.6之前,还没有使用到泛型,Java类型分为原始类型、复杂类型...

  • spring 泛型处理

    java 泛型基础 泛型类型:泛型类型是在类型上参数化的泛型类或接口 泛型使用场景编译时前类型检查。定义为 Col...

  • Java泛型基础

    Java泛型基础 1. 认识泛型 泛型是在JDK1.5之后增加的新功能. 泛型可以解决数据的安全性问题, 主要的原...

  • 一文带你认识Java泛型基础

    Java泛型基础 1. 认识泛型 泛型是在JDK1.5之后增加的新功能. 泛型可以解决数据的安全性问题, 主要的原...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Java基础(一)

    Java要点1 JAVA基础 1.对抽象、继承、多态的理解 2.泛型的作用及使用场景 1.Java泛型是JDK1....

  • 详解Java泛型之3——十分钟理解泛型擦除

    前面我们介绍了泛型以及通配符的基础概念,可以参考文章: 详解Java泛型之1——入门泛型必懂的知识点[https:...

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

网友评论

      本文标题:Java基础-泛型

      本文链接:https://www.haomeiwen.com/subject/djtmiqtx.html