Java-泛型程序设计

一.为何要使用泛型:

  可以被很多不同类型的对象所重用。比那些直接使用Object变量,然后强制类型转换的代码具有跟好的安全性和可读性。

使用类型参数好处:

  1.可以将需要使用的类型提前声明,如:ArrayList<String> newlist = new ArrayList<String>();

  2.可以告知这个类适用于什么类型,当调用对应的get()方法的时候,不需要进行强制类型转换,编译器本事就知道其对应的类型。

当实现一个泛型的时候非常不容易,因为你需要知道这个这个类对应的所有用途及其类型,所以java提供了通配符类型,来解决这个问题。

 

二.定义简单泛型类

  泛型类,就是指具有一个或者多个类型变量,也就是说这个类适应这几种类型,对于之后在那类来说,我们只关注泛型,而不会为数据类型转换烦恼。

  类型变量使用大写形式,且比较短,这是很常见的。在java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字和值的类型。T(需要时还可以用临近的字母U和S)表示类型。

  使用类型变量T,用<>括起来,放在类名后面。这个泛型可以有多个类型变量,如<T,U>。

 

三.定义泛型方法

  泛型方法既可以在普通类中,也可以在泛型类中,定义方式是在方法名前加<T> T,说明该方法是泛型方法。如下:

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

  当调用一个泛型方法时, 在方法名前的尖括号中放入具体的类型。如下:

String middle = ArrayAlg.<String>getMiddle("john", "Q.", public ");

  方法调用中可以省略类型参数, 编译器有足够的信息能偶推断出所调用的方法,如下:

String middle = ArrayAlg.getMiddle("john", "Q.", public ");

  对于泛型方法的引用都没有问题。 偶尔, 编译器也会提示错误。如下,编译器将会自动打包参数为 1个 Double 和 2个Integer 对象,而后寻找这些类的共同超类型。事实上, 找到2个这样的超类型:Number 和 Comparable 接口, 其本身也是一个泛型类型。 在这种情况下, 可以采取的补救措施是 将 所有的参数写为 double 值。

double middle = ArrayAlg.getMiddle(3.14, 0, 1729);

 

四.类型变量的限定

  有的时候,比如对于特定的方法执行特定的操作,但是该操作不适用于一些类型,这时可以对类型变量T设置限定,可以使其集成特别的类或者接口(在这里对于接口也是使用继承,因为使用extends更加接近子类的意思)

  一个类型变量或通配符可以有多个限定,限定类型用“&” 分隔,而用逗号来分隔类型变量。在java继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,但必须是第一个。如:T extends Comparable & Srializable

五.泛型代码和虚拟机

Java泛型转换的事实:  

  JVM中没有泛型,只有普通的类和方法。

  所有的类型参数都用他们的限定类型替换(如果没有类型参数,则使用Object类型),这个过程称为擦除(erased),擦除类型变量,并替换为限定类型。

  桥方法被合成来保持多态。

  有时为保持类型安全性,需要插入强制类型转换。

 

六.约束与局限

使用Java泛型时需要考虑一些限制,这些限制大都是由类型擦除引起的。

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

   不能用类型参数来代替基本类型。就是没有Pair<double>,只有Pair<Double>。当然主要是原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double的值。

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

     虚拟机中的对象总有一个特定的非泛型类型。因此,所有类型查询只产生原始类型。

    如:if(a instanceof Pari<String>)是错误的,因为只能查询原始类型,即Pari,if(a instanceof Pari<T>)是错误的

3. 不能创建参数化类型的数组:

       Couple<Employee>[] couple = new Couple<Employee>[5] ; 这种声明式不合法的。如果要存放参数化类型对象的集合,可以考虑使用ArrayList<Couple<Employee>>进行声明,而且Java中建议优先使用集合,这样既安全又有效。

4.Varargs警告:

   向参数个数可变的方法传递一个泛型类型的实例,为调用这个方法,虚拟机必须建立一个Pair<String>数组,这就违反了前面的规则,不过,对于这种情况有所放松,只会得到一个警告,而不是错误。

     可以使用注解@SuppressWarnings("unchecked")或@SafeVarargs抑制这个警告。

5.不能实例化类型变量:

     不能使用像new<T>(...),new t[...]或T.class这样的表达式中的类型变量。

6.泛型类的静态上下文中类型变量无效:

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

7.不能抛出或捕获泛型类的实例:     

     在Java中, public class GenericException <T> extends Exception {...} 这种泛型类扩展子Throwable是不合法的,不能通过编译器。不能再catch子句中使用类型参数。

     java 异常处理的一个基本原则是,必须为所有已检查异常提供一个处理器。不过可以利用泛型消除这个限制。

8.注意擦除后的冲突

 

七.泛型类型的继承规则

   在使用泛型时,需要了解一些有关继承和子类型的准则。

  如:Manager是Employee的子类,那么Pair<Manager>是Pair<Employee>的一个子类吗? 答案是:“不是”

  规则:无论S与T有什么联系。通常,Pair<S>与Pair<T>没有什么联系。

  永远可以将参数化类型转换为一个原始类型。

 

 

 

 

八.通配符类型

1.为什么使用通配符:

  固定的泛型类型系统使用起来并没有那么友好,Java的设计者发明了一种巧妙的解决方法:通配符类型。

  泛型通配符(?)和类型变量(T)的区别——在任何泛型类型(泛型变量声明中)vs某一种泛型类型(泛型类实现中)?可以在声明变量时使用,例如:

    Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);

    Pair<? extends Employee> wildcardBuddies=managerBuddies;

2.通配符概念:

  通配符类型中,允许类型参数变化。

  例如:通配符类型:Pair<? extends Employee>

    表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>

  即前面说到的问题:不能将Pair<Manager>传递给Pair<Employee>,解决方法是:使用通配符类型。

  Pair<? extends Employee>中的方法似乎是这样:

    ? extends Employee getFirst()

    void setFirst(? extends Employee)

  这样将不能调用setFirst方法,编译器只知道需要某个Employee的子类型,但不知道具体是什么类型,所以拒绝传递任何特定的类型。

  用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee的引用完全合法

这就是引入有限定的通配符的关键之处。

 

3.通配符的超类型限定

  通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定:? super Manager。这个通配符限制为Manager的所有超类型

  引入super的原因:带有超类型限定的通配符的行为之前介绍的的Pair<? extends Employee>行为恰恰相反,可以为方法提供参数,但不能使用返回值。

  直观来说,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

4.无限定通配符

  还可以使用无限定的通配符,如:Pair<?>,它有以下方法:

    ? getFirst()

    void setFirst()

   getFirst的返回值只能赋给一个Object。setFirst方法不能被调用。

5.通配符捕获

  通配符不是类型变量,因此不能在编写代码时使用“?”作为一种类型,如:? t=p.getFirst();

如何临时保存一个通配符类型变量,需要一个辅助方法,如下:

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

这里使用swapHelper方法的参数T捕获通配符。

通配符捕获只有在有许多限制的情况下是合法的,编译器必须确定通配符表达的是单个、确定的类型。如:ArrayList<Pair<?>>中的通配符就不能通过ArrayList<Pair<T>>捕获。数组列表可以保存两个Pair<?>,分别针对?的不同类型。

 

九.反射和泛型

  反射允许你在运行时分析任意对象。但如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。

  下面将了解利用反射可以获得泛型类的什么信息。

1.使用Class参数进行类型匹配

  现在,Class类是泛型的,类型参数十分有用,这是因为它允许Class<T>方法的返回类型更加具有针对性。

  Class<T>中的方法就使用了类型参数:

T newInstance()——返回一个实例,这个实例所属的类由默认的构造器获得。

T cast(Object obj)——如果给定的类型确实是T的一个子类型,cast方法就会返回一个现在声明为类型T的对象

T[] getEnumConstants()——如果这个类不是enum类或类型T的枚举值的数组,返回null

Class<? super T>get SuperClass()

Constructor<T> getConstructor(Class... parameterTypes)

Constructor<T> getDeclaredConstructor(Class... parameterTypes)

 

  有时,匹配泛型方法中的Class<T>参数的类型变量很有实用价值:

public static <T> makePair(Class<T> c) throws InstantiationException         
{
       return new Pair<>(c.newInstance(),c.newInstance());
}

  如果调用makePair(Employee.class),makePair方法的类型参数T同Employee匹配,并且编译器可以推断出这个方法将返回一个Pair<Employee>

3.虚拟机中的泛型类型信息

  Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。例如,原始的Pair类知道源于泛型类Pair<T>,但不知道具体的类型变量。

  类似地,方法: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[])

posted @ 2021-06-01 21:34  Xiarsu  阅读(183)  评论(0)    收藏  举报