Java泛型程序设计

泛型程序设计的意义:编写的代码可以被很多不同类型的对象所重用。

好处:出现编译错误比类在运行时出现类的强制类型转换异常要好得多。

程序员的任务:预测出所用类的未来可能有的所有用途。

1、定义简单泛型类

public class Pair<T>{

...

}

ps:泛型类相当于普通类的工厂

2、定义泛型方法

class ArrayAlg{

     public static <T> T getMiddle(T ...a){

     ...

     }

}

ps:类型变量放在修饰符后面,返回值前面

调用方法,ArrayAlg.<String>getMiddle(...);

3、类型限定

限定类型变量必须是,实现了Comparable接口的类

public static <T extends Comparable> T min(T[] a) ...

public static <T extends Comparable & Serializable> T min(T[] a) ...

4、类型擦除

泛型实现的早期版本中,是向后兼容的。

向后兼容:在1.0版本中,可以使用之后,甚至未来版本的功能。

类型擦除:擦除类型变量,并替换为限定类型(无限定的变量用Object)

若有两个限定类型,则类型擦除为,第一个限定类型,对于第二个限定类型,编译器必要时则插入强制类型转换,为提高效率,应将标签接口放在限定类型列表最后。

与C++模板的区别,C++中每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。

(1)翻译类型表达式

Pair<Employee> buddies...

Employee buddy=buddies.getFirst();

编译器把这个方法的调用,分别解释为:

  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

(2)翻译泛型方法

class DateInterval extends Pair<LocalDate>{

       public void setSecond(LocalDate second){

               ....

      }

}

DateInterval interval = new DateInterval();

Pair<LocalDate> pair = interval;

pair.setSecond(aDate);

问题来了,Pair<LocalDate>类里的setSecond(LocalDate second)方法,经过类型擦除后,变为setSecond(Object second),这个时候,interval怎么通过父类的这个类型擦除后的方法,去调用自己重写的方法呢?

编译器是这样解决的:

  • 在DateInterval中生成一个桥方法,
  •  public void setSecond(Object second){
  •                setSecond((Date)second);
  • }
  • 实质上,合成的桥方法,调用了新定义的方法。
  • 产生的问题:编译器可能产生两个仅返回类型不同的方法,但虚拟机能正确处理。

小结:

虚拟机中没有泛型,只有普通的类和方法。

所有的类型参数都用它们的限定类型替换。

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

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

5、泛型编程的约束和局限

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

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

如,a instanceof Pair<String> //错误

Pair<String> stringPair;

Pair<Employee> employeePair;

stringPair.getClass() == employeePair.getClass(); //总是相等,它们都将返回Pair.class

(3)不能创建参数化类型的数组

如,Pair<String>[] table = new Pair<String>[10]; //错误,类型擦除后,这个数组存什么都可以了。

但可以,Pair<String>[] table = (Pair<String>[])new Pair<?>[10]; //但不安全,因为可以存Pair<Emplyee>

如果需要获取,一个参数化类型对象的集合,可以这样使用,ArrayList<Pair<String>>。

(4)@SafeVarargs直接标注addAll方法

Collection<Pair<String>> table = ...;

Pair<String> pair1 = ...;

Pair<String> pair2 = ...;

//Java 7 之后,直接用了SafeVarargs标注了addAll和array,消除了创建泛型数组的有关限制

addAll(table,pair1,pair2);

(5)不能实例化类型变量

new T(),new T[],T.class,T.class.newInstance等表达式均非法。

解决办法一:

Pair<String> p = Pair.makePair(String::new);

public static<T> Pair<T> makePair(Supplier<T> constr){

     return new Pair<>(constr.get(),constr.get());

}

ps:Supplier<T>是一个函数式接口,表示一个无参数而且返回类型为T的函数。

解决办法二:

public static<T> Pair<T> makePair(Class<T> cl){

     return new Pair<>(cl.newInstance(),cl.newInstance());

}

Pair<String> p = Pair.makePair(String.class);

ps:Class类本身是泛型,String.class是一个Class<String>的唯一实例。

(6)构造泛型数组方法

方法一:

String[] ss = ArrayAlg.minmax(String[]::new, "Tom","Dick","Harry");

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a){

   T[] mm = constr.apply(2);

}

方法二,利用反射机制:

public static <T extends Comparable> T[] minmax(T... a){

   T[] mm = (T[])Array.newInstance(a.getClass().getComponentType() , 2);

}

(7)泛型类的静态上下文中类型变量无效

public class Singleton<T>{

    private static T singleInstance; // 错误

}

ps:类型擦除后,只剩下Singleton类和 singleInstance域。

(8)不能抛出或捕获泛型类的实例

1、继承throwable的类不能是泛型类。

2、catch子句里,不能catch类型变量。

但是,在异常规范中使用类型变量是允许的。

如:public static <T extends Throwable> void do work(T t) throws T{

...

}

(9)可以消除对受查异常的检查

Java异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。

但是,类似于下面这种写法:

@SuppressWarnings("unchecked")

public static <T extends throwable> void throwAs(Throwable e) throw T{

     throw (T)e;

}

new Thread(){

    public void run(){

         try{

           ....

       }catch(Throwable t){

              Block.<RuntimeException>throwAs(t);

        }

   }

}

ps:一般来说,我们必须捕获线程run方法中的所有受查异常,但run方法声明为不抛出任何受查异常,我们只是抛出异常,并"哄骗"编译器,让它认为这不是一个受查异常。

(10)注意擦除后的冲突

泛型规范原则:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。

class Employee implements Comparable<Employee>;

class Manager extends Employee implements Comparable<Manager>;

ps:以上两个定义是错误的,原因是有可能和产生的桥方法,产生冲突。

6、泛型类型的继承规则

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

如:Pari<Employee> 可以转换为Pair,但可能会产生错误。

(2)泛型类可以扩展或实现其他的泛型类。

(3)ArrayList<Manager>可以转换为List<Manager>,但不能转换为List<Employee>。

7、通配符类型

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

子类型限定的通配符:

Pair<Manager>是Pair<? extends Employee>的子类型。

? extends Employee限定:可以用于getter方法,不能用于setter方法。

ps:getter返回对象回来赋给Employee类型,完全可以

setter类型,不可以的理由是,不知道需要哪个子类

超类型限定的通配符:

void setFirst(? super Manager);

? super Manager getFirst();

setter方法,只能传入Manager或其子类,不能传入父类。

getter方法,只能获取到Object对象。

一般的准则:setter方法一般用超类型限定的通配符,getter方法一般用子类型限定的通配符。

另一种应用:

public static <T extends Comparable<T>> T min(T[] a);

T extends Comparable<T>使用得更彻底,工作地更好。

但有ChronoLocalDate是LocalDate的父类,ChronoLocalDate扩展了Comparable<ChronoLocalDate>接口。因此使得,LocalDate类也扩展了Comparable<ChronoLocalDate>接口。

所以,可以写成,public static <T extends Comparable<? super T>> T min(T[] a) 来解决问题。

ps:上述声明,只为排除调用参数上的不必要的限制,若是一名库程序员,一定要习惯于通配符,否则在代码中就需要多处,添加强制类型转换直至代码可以编译。

无限定通配符:

Pair<?>的返回?的getter方法,只能返回Object,相应的setter(?)方法,不能被调用。

Pair<?>和Pair的本质:可以用任意的Object对象调用原始Pair类的setObject方法。

用法:避免使用通配符类型。

setFirst(null);

public static boolean hasNulls(Pair<?> p){

     return p.getFirst()==null || p.getSecond()==null;

}

猜你喜欢

转载自blog.csdn.net/qq_27437197/article/details/85203071