泛型

《Java Core》ed.11 学习笔记

为什么使用泛型

泛型编程的意义就在于写不同类型的对象都可以复用的代码,例如ArrayList可以接受所有类型的对象(实际上Java在有泛型之前就有了ArrayList类)

类型参数的优点

ArrayList为例,在没有泛型之前

class ArrayList {
      private Object[] elementData;

      public Object get(int i) {};
      public void add(Object o) {};
}

在没有泛型之前,所谓的泛型编程是通过继承实现的,即所有的类都是Object类的子类,上面的ArrayList在使用时需要类型转换。泛型提供了类型参数。所以现在var files = new ArrayList<String>();。提高了代码的可读性

如果不使用var而是特定的数据类型来声明ArrayList,那么可以使用菱形语法,即ArrayList<String> files = new ArrayList<>();

Java9中支持了新的菱形语法即

ArrayList<String> passwords = new ArrayList<>()
{
      @Override
      public String get(int n) { return super.get(n).replaceAll(".", "*"); }
};

相当于写了一个匿名子类,这里面的方法可以覆写ArrayList的方法,即集合元素是存在这个匿名子类对象中,调用的方法也是先从这里面寻找

编译器对于泛型的支持也很好,知道方法返回的类型是什么。也知道传入方法的参数类型是什么。这要比用Object类的方式安全得多

易读安全是泛型的意义所在

谁想成为泛型程序员(?想但是做不到)

使用泛型类的人希望这个类可以适应所有类型(没有各种限制和错误信息),所以设计泛型类的人要时刻考虑这个类未来会遇到的各种情况。举例:将ArrayList<Manager>中的元素全部添加到ArrayList<Employee>中,此时,后者的addAll()可以使用前者当参数,但是反过来就不行(类型不能转换)。此时,Java类库的设计人员发明了通配符类型,这样可以使方法更加灵活

泛型编程的三个阶段:

  1. 使用
  2. 碰到问题开始研究泛型的原理解决问题
  3. 自己实现泛型类

应用程序员不会写很多泛型类,只有在适用对象为Object或者类似于Comparable接口时才需要用到泛型

定义一个简单的泛型类

举例

public class Pair<T>
{
      private T first;
      private T second;
      public Pair() { first = null; second = null; }
      public Pair(T first, T second) { this.first = first; this.second = second; }
      public T getFirst() { return first; }
      public T getSecond() { return second; }
      public void setFirst(T newValue) { first = newValue; }
      public void setSecond(T newValue) { second = newValue; }
}

类型参数在类名后,类型变量可以有多个,例如public class Pair<T, U> {}

类型变量在整个类的定义中可以用来指定方法返回类型、成员变量类型、局部变量

一般实践:类型参数的名称尽量短(E-element,KV-key、value,T、S、U-任意类型)

实例化泛型类型的对象,只要用具体类型来替换类型参数即可Pair<String>

public class C {

    public static void main(String[] args) {
        String[] words = new String[10];

        words[0] = "abc";
        words[1] = "dfsas";
        words[2] = "dd";
        words[3] = "vdss";
        words[4] = "as";
        words[5] = "vdvvv";
        words[6] = "aaas";
        words[7] = "ee";
        words[8] = "a";
        words[9] = "ffdfd";

        Arrays.sort(words, ((Comparator<? super String>) String::compareTo).reversed());
        Pair<String> pair = new Pair<>(words[0], words[words.length-1]);

        System.out.println(pair.getFirst() + " " + pair.getSecond());
    }
}

泛型方法

Java允许定义一个带有类型参数的方法

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

上面代码是一个泛型方法,定义在一个普通类中,这里类型参数是在修饰符后,返回值前的。在普通类和泛型类中都可以定义泛型方法。调用泛型方法时,语法是在方法名前加<Type parameter>。一些情况下(大部分情况)这个类型参数可以省略,编译器可以进行类型推断

泛型的类型推断一般来说工作顺利,出问题时需要手动解决(废话),例如上面代码上下文中出现double go = ArrayAlg.getMiddle(1.2, 24, 4);这时编译器可能会把参数自动装箱成Double对象和Integer对象,然后找它们共同的超类,发现Number类和Comparable接口(本身就是泛型类型),这时需要将参数改成double值

类型变量的边界设置(bound)

例子:

class ArrayAlg
{
      public static <T> T min(T[] a) // almost correct
      {
            if (a == null || a.length == 0) return null;
            T smallest = a[0];
            for (int i = 1; i < a.length; i++)
            if (smallest.compareTo(a[i]) > 0) smallest = a[i];
            return smallest;
      }
}

这里的smallest的类型是T,我们要确保它一定有compareTo方法,那么就要限制T类型必须实现了Comparable接口。代码为public static <T extends Comparable> T min(T[] a)...。这里编译器会有警告信息,之后学习通配符语法再改进

之所以用extends而不是implements,是其含义更接近子类型的概念,而且类型参数可以是类的子类也可以是实现了某种接口的类型,且Java设计者不想添加新的关键字。T extends Comparable & Serializable,表示T有两种类型限制。不用,是因为已经用作分隔类型参数

在限制(bound)列表中,至多一个类类型、且这个类类型必须在extends后的第一个

泛型代码和虚拟机

虚拟机中没有泛型类型的对象,所有的对象都属于一般类型。在实现泛型的早期版本里,甚至可以编译在1.0虚拟机上运行class文件中使用泛型的程序(不理解,待什么时候啃到深入理解JVM再来细看)

类型擦除

无论何时定义一个泛型类型,都会自动提供一个raw类型,它的名字就是泛型类型的名字,且所有类型参数被移除。类型参数被擦除并且被(bound)边界类型或Object类型(没有边界的类型变量)代替

这样做的结果就变成了一个一般类。程序中可能包括Pair<String> Pair<LocalDate>,但是擦除让它们都变成了原始的Pair类型

Interval<T extends Comparable & Serializable>,T会被替换成Comparable类型,如果这两个限制类型写反了,编译器会替换成后者,必要时再进行类型转换,所以为了提高效率,应该把tagging interface(没有方法的接口)放在限制类型最后面

翻译泛型表达式

在调用泛型方法时,虚拟机的两条转换原则

  1. 先调用原始类型,例如Pair的方法
  2. 对返回值进行类型转换

在访问泛型属性(public)时,Java也会提供类型转换

翻译泛型方法

对于泛型方法,一样有类型擦除。这种机制在泛型类的子类中的方法有一些问题,就是子类如果覆写了父类的方法,子类会有一个覆写的方法和父类的一个返回值是Object的方法(假设返回值是T)

在Java中判断方法签名是方法名+参数类型/个数/顺序,但是虚拟机中是通过返回值类型和参数类型来判断的,这样的话JVM在泛型中并不会出错(自动添加了bridged method,在父类方法中调用子类真正的方法)

在此记住4个知识点:

  1. 虚拟机中没有泛型,只有一般类和方法
  2. 所有类型参数都被它们的限制类型代替
  3. 桥梁方法是为了维持多态
  4. 类型转化为了保证类型安全是必要的

调用老代码

基本可以直接做类型转换,警告信息可以通过注解@SuppressWarnings("unchecked")解决,即不检查域中的代码

限制

大多数限制都是由类型擦除带来的

类型参数不能用基本类型初始化

因为类型擦除,基本数据类型没有超类

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

虚拟机中的对象总是有一个非泛型类型。所以if (a instanceof Pair<String>)只能说明a是否是Pair类型的对象,包括类型转换,都会收到编译器的警告或者错误信息。getClass()也总是返回原始类型

不能创建含有类型参数的数组

var table = new Pair<String>[2]是错误的。在类型擦除后,table的类型为Pair[],可以转换成Object[],如果存储任何一个错误类型的数据进数组,数组都会抛出ArrayStoreException异常。这种机制对于泛型对象复制检查又不高效,objarray[0] = new Pair<Employee>();是合法的。所以不要创建这种数组,且错误只会在创建数组时出现,声明是可以的

如果想收集泛型类型的对象,可以使用ArrayList<Pair<String>>

可变参数的警告信息

感觉基本碰不到。不深究了

将泛型类型的实例当作可变参数传入方法中会怎样

可变参数其实是将所有提供的参数合成一个数组,此时违反了上面那条规则,但是这里又允许了。但是会报警告信息,此时有两种方法解决

  1. @SuppressWarnings("unchecked")
  2. java7之上,@SafeVarargs,这个注解只能放在方法和构造器上,且方法的修饰符要是(static、final、private(java9以上))如果方法可以被继承,这个注解失去意义。这个注解还可以创建泛型实例的数组(执着)@SafeVarargs static <E> E[] array(E... array) { return array; }

不能实例化类型变量

不能在表达式中使用类型变量first = new T();因为类型擦除使得它变成了new Object()

最好的处理方式(Java8),可以创建一个方法,让调用者提供一个构造器表达式,例如

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

// call
Pair<String> pair = Pair.makePair(String::new);

传统方式是使用反射来构造泛型对象,通过Constructor.newInstance()

public static <T> Pair<T> makePair(Class<T> cl)
{
      try {
            return new Pair<>(cl.getConstructor().newInstance(),
                  cl.getConstructor().newInstance());
      }
      catch (Exception e) { return null; }
}

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

不能构造一个泛型数组

和不能构造一个泛型实例相似,也不能实例化一个泛型数组。数组会使用null值填充,数组也带有类型,在虚拟机中这可以帮助监控数组存储,当类型擦除后

public static <T extends Comparable> T[] minmax(T... a)
{
      T[] mm = new T[2]; // ERROR
      . . .
}

这时创建的是一个Comparable数组,如果这个泛型数组是私有的,类内调用的,那么可以使用类型转换使用,但是如果一个方法返回泛型数组,方法内部在类型擦除后是Object或者它的类型参数的父类的数组,此时在返回值的时候会报ClassCastException异常,编译不会报错

这种情况可以让调用者提供一个构造器表达式

String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
// 方法签名
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr
{
      T[] result = constr.apply(2);
      . . .
}
// 调用
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");

老式的方法还是使用反射

public static <T extends Comparable> T[] minmax(T... a)
{
      var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
      . . .
}

ArrayListtoArray方法是要返回一个T[],因为它没有component类型,所以有两种方法来调用

Object[] toArray()
T[] toArray(T[] result)

在第二种方法中,如果result参数的数组够大,那么就会用这个数组,否则会创建一个新数组(使用result的component类型)

在泛型类的静态上下文(context)中是无效的

在静态成员变量或方法中是不能引用泛型类型的。如果可以的话,那么声明一个Singleton<Random>可以共享一个随机数生成器,声明一个Singleton<JFileChooser>可以分享一个file chooser dialog。但是这不被允许。类型擦除之后只有一个Singleton,也只有一个SingletonInstance静态成员变量

不能throw或者catch泛型类的实例

不可以定义一个继承Exception类的泛型类,也不能在catch语句中使用类型参数。在异常处理代码块中是可以使用泛型类型实例的

可以打破受检异常的检查

Java异常的基石原则是所有受检异常都要有一个处理机制。但是使用泛型可以打破这个原则

@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
      throw (T) t;
}
// 如果这个方法在接口Task里
// 调用
Task.<RuntimeException>throwAs(e);
// 此时编译器认为这个e异常一定是非受检异常
try
{
      do work
}
catch (Throwable t)
{
      Task.<RuntimeException>throwAs(t);
}

这个机制用来解决一个烦人的问题(要运行线程中的代码,需要把代码放在实现了Runnable接口的类的run方法中,但是这个方法不允许抛出受检异常),可以提供一个适配器,它的run方法可以抛出任何异常给Runnable

interface Task
{
      void run() throws Exception;
      @SuppressWarnings("unchecked")
      static <T extends Throwable> void throwAs(Throwable t) throws T
      {
            throw (T) t;
      }
      static Runnable asRunnable(Task task)
      {
            return () ->
            {
                  try
                  {
                        task.run();
                  }
                  catch (Exception e)
                  {
                        Task.<RuntimeException>throwAs(e);
                  }
            };
      }
}

// Test
var thread = new Thread(Task.asRunnable(() ->
{
      Thread.sleep(1000);
      System.out.println("Hello, World!");
      throw new Exception("Check this out!");
}));
thread.start();

PS:感觉这个还挺实用的,在线程运行中能抛出任意类型异常比较重要

意识到擦除后的冲突

在泛型类型擦除后有冲突的情况是不合法的public boolean equals(T value),这个方法定义之后,类中会存在两个equals()方法,另一个是从Object类中继承下来的方法。然后在类型擦除之后,留下的equals()方法和继承下来的方法直接冲突了

解决方法就是改方法名(唉)

泛型的说明文档有一个规则:“为了支持擦除之后的转换,所以加了一条限制:一个类或者类型变量不能同时是两个被一个接口不同类型参数化的接口类型的子类型”(真的绕)

class Employee implements Comparable<Employee> { . . . }
class Manager extends Employee implements Comparable<Manager> { . . .} // ERROR

此时Manager就实现了两个不同类型参数化的同一个接口

这件事情会和综合的桥梁方法冲突。实现了Comparable<X>接口的类会有一个public int compareTo(Object other)方法,一个类不能有两个一模一样的这个方法

泛型类型的继承规则

一般情况下,Pair<S>Pair<T>是没有任何关系的。无论S和T有什么关系

泛型的继承关系是原始类型的继承关系,例如ArrayList<T>实现了List<T>接口,所以ArrayList<String>可以转为List<String>

通配符类型

僵化的泛型系统并不好用,Java的设计者想出了个有创意的(ingenious)但缺乏了一些安全性办法(escape hatch--紧急出口),就是通配符类型

通配符概念

通配符类型代表类型参数是可变的

Pair<? extends Employee>

表示任何Pair类型参数都是Employee的子类

不能通过通配符的对象引用来破坏类的完整行为

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

最后一条语句会有编译期错误。是因为

? extends Employee getFirst()
void setFirst(? extends Employee)

编译器只知道传入setFirst方法的类型是Employee的子类,但是不知道具体是什么类型,所以它拒绝所有的指定类型参数传入。现在我们有了安全的访问方法和不安全的修改方法的实现形式

通配符的超类边界(bound)

? super Manager,即泛型类型必须是Manager以及它的超类

这里应用层面就不多写了,和extend的行为相似且作用相反,访问方法可以使用Object接受,修改方法可以传入超类对象

无边界通配符(unbounded)

? 这个通配符标识下的访问方法只能用Object接收,而修改方法永远不能被调用,甚至是使用Object。可以使用setFirst(null)

这个用法主要是用来写一些和类型参数无关的方法

public static boolean hasNulls(Pair<?> p)
{
      return p.getFirst() == null || p.getSecond() == null;
}

捕获通配符

public static void swap(Pair<?> p) {
      ? t = p.getFirst(); // ERROR
      p.setFirst(p.getSecond());
      p.setSecond(t);
}

通配符并不是一种类型,所以不能把?当作类型。解决这个问题可以使用帮助方法

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); }
// 这里,T将通配符的类型捕获了

上面的代码比较冗余,但是有更自然的例子

public static void maxminBonus(Manager[] a, Pair<? super Manager> result)
{
      minmaxBonus(a, result);
      PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
}

反射和泛型

反射使得在运行时分析任何对象。如果对象是泛型类型实例化出来的,不能得到泛型类型参数的更多信息,因为它们被擦除了,这一章介绍使用反射能在泛型类中获得的信息

泛型Class

Class类现在是泛型实现的,例如String.class其实是Class<String>的实例对象

// 调用这个类的无参构造器,避免类型转换
T newInstance()  
// 只要是T的子类即可转换,否则报BadCastException
T cast(Object obj)
// 如果这个类不是枚举,则返回null,否则是T类型的数组
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.newInstance(), c.newInstance());
}
// 此时编译器能推断出类型是Employee
makePair(Employee.class)

虚拟机中的泛型类型信息

虽然有类型擦除,原始类依旧知道一些泛型的信息,例如Pair类知道它是从Pair<T>而来,即使这个类型的对象并不知道构建的是什么Pair<String>还是Pair<Employee>

public static <T extends Comparable<? super T>> T min(T[] a)
// 类型擦除后
public static Comparable min(Comparable[] a)

使用反射API可以确定

  1. 有类型参数叫T的泛型方法
  2. 有子类边界本身是泛型类型的类型参数
  3. 有通配符参数的边界类型
  4. 通配符参数有子类边界
  5. 泛型方法接收泛型数组参数

可以重新构建关于那些实现者实现的泛型类和泛型方法的一切。然而,无法获取对于特定对象或方法调用来说类型参数是怎么处理的

使用Type接口来表示泛型类型的声明

类型字面值

在需要通过值的类型来驱动程序行为时(希望用户指明保存特别类的一个对象的方式)。在这种场景下,类型擦除使得ArrayList<Integer>ArrayList<String>是完全一样的类型

可以捕获一个Type类型接口的实例,构造一个匿名子类

var type = new TypeLiteral<ArrayList<Integer>>(){} // note the {}
// TypeLiteral构造器捕获了泛型的超类
class TypeLiteral
{
      public TypeLiteral()
      {
            Type parentType = getClass().getGenericSuperclass();
            if (parentType instanceof ParameterizedType)
            {
                  type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
            }
            else
            throw new UnsupportedOperationException("Construct as new TypeLiteral<. . .>(){}");
      }
      . . .
}

看到这里实在是有点晕了,只能看出来是要拿到泛型的类型信息,以后有新的体会再来补充吧

posted on 2020-11-17 17:06  老鼠不上树  阅读(260)  评论(0)    收藏  举报