Java核心技术卷1: 泛型程序设计

为何要使用泛型程序设计

泛型程序设计意味着编写的代码能够被不少不一样类型的对象所重用.java

在Java中增长泛型类以前, 泛型程序设计是用继承实现的. 例如ArrayList类只维护一个Object引用的数组:数组

public class ArrayList {
  private Object[] elementData;
  public Object get(int i) {...}
  public void add(Object o) {...}
}

这样实现有两个问题.安全

1. 当获取一个值时必须进行强制类型转换.函数

ArrayList first = new ArrayList();
...;
String filename = (String) files.get(0);

2. 没有进行错误检查, 能够向数组列表中添加任何类的对象.测试

files.add(new file("..."));

而泛型提供了一个更好的解决方案: 类型参数:this

ArrayList<String> files = new ArrayList<String>();

备注:spa

问题1: 为何针对files.get(0), 强制转换为String能够成功?翻译

答: 虽然ArrayList存储的是Object, 但它也会存储额外的信息, 用来肯定所存储的元素类型, 因此才能保证强制转换的正确性.设计

问题2: 为何是以Object为引用?code

答: 由于Object是Java中的最原始的类型, 除了基本数据类型外, 全部的对象均派生于Object, 即全部的对象均可向上转型为Object.

 

定义简单泛型类

package pair1;

/**
 * Created by lgt on 16/7/9.
 */
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;}
}

备注: 在Java中, 使用变量E表示集合的元素类型, K和V分别表示表的关键字与值的类型, T, U和S表示任意类型.

 

泛型方法

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

而后, 咱们能够这样进行调用:

String middle = ArrayAlg.<String>getMiddle("hello", "world", "Java");

备注: 

1. 泛型方法既能够定义在普通类中, 也能够定义在泛型类中.

2. 若是泛型方法的参数类型能够推导出来, 则可省略, 如

String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

可是在没法推导出来时候, 例如double和int, 则仍是须要类型参数.

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

 

类型变量的限定

有时, 类或方法须要对类型变量加以约束. 例如咱们计算数组中的最小元素:

class ArrayAlg {
  public static <T> T min(T[] a) {
    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;
  }
}

这里存在一个问题在于: 调用compareTo方法的对象必须实现了Comparable接口才行, 而T类型并不肯定是否实现了Comparable接口.

咱们须要扩展T类型:

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

而一个类型变量或通配符能够有多个限定:

T extends Comparable & Serializable

一个实际的例子:

package pair2;

import pair1.Pair;
/**
 * Created by lgt on 16/7/9.
 */
public class PairTest2 {
  public static void main(String[] args) {
    String[] strs = new String[]{"hello", "world", "i", "love", "coding"};
    Pair<String> mm = ArrayAlg.minmax(strs);
    System.out.println("min=" + mm.getFirst());
    System.out.println("max=" + mm.getSecond());
  }
}

class ArrayAlg {
  public static <T extends Comparable> Pair<T> minmax(T[] a) {
    if (a == null || a.length == 0) return null;
    T min = a[0];
    T max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (min.compareTo(a[i]) > 0) min = a[i];
      if (max.compareTo(a[i]) < 0) max = a[i];
    }
    return new Pair<>(min, max);
  }
}

 

泛型代码和虚拟机

虚拟机没有泛型类型对象--全部对象都属于普通类.

不管什么时候定义一个泛型类型, 都自动提供了一个相应的原始类型. 原始类型的名字就是删去类型参数后的泛型类型名. 擦除类型变量, 并替换为限定类型.

如Pair<T>的原始类型以下:

public class Pair {
  private Object first;
  private Object second;
  ......
}

而假定对T进行了扩展, 则为扩展的类型:

public class Interval<T extends Comparable & Serializable> implements Serializable {}

其中原始类型为: Comparable

但若是某些变量的类型为Serializable, 则编译器在必要时候进行强制转换(Comparable --> Serializable)

翻译泛型表达式

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

擦除getFirst的返回类型后将返回Object类型. 编译器自动插入Employee的强制类型转换. 即编译器把这个方法调用翻译为两条虚拟机指令:

1. 对原始方法Pair.getFirst的调用.

2. 将返回的Object类型强制转换为Employee类型.

因为对象buddies会存储实际类型的信息(Employee), 因此能够保证强制类型转换成功.

翻译泛型方法

类型擦除也会出如今泛型方法中:

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

通过类型擦除后变成:

public static Comparable min(Comparable[] a);

但方法的擦除带来两个复杂的问题, 例如:

class DateInterval extends Pair<Date> {
  public void setSecond(Date second) {...}
}

这里因为Pair也有setSecond(Date d)方法, 因此它们为一样的方法, 动态运行时候能够绑定变量的类型, 决定调用哪一个方法(多态).

可是因为类型擦除后:

class DateInterval extends Pair {
  public void setSecond(Date second) {...}
}

Pair中的setSecond为: public void setSecond(Object second) {...}, 因此没法进行动态绑定(Date和Object为不一样的类型, 即此时两个setSecond为不一样的方法).

因为类型擦除致使多态失效. 因此咱们须要用桥方法将两个setSecond方法"多态"起来:

class DateInterval extends Pair {
  public void setSecond(Date second) {...}
  public void setSecond(Object second) {
    setSecond((Date) second);
  }
}

编译器生成了第二个setSecond方法, 从而解决了擦除致使多态失效的问题.

总结以下:

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

2. 全部的类型参数都用它们的限定类型替换.

3. 桥方法被合成保持多态.

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

 

约束与局限性

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

例如没有Pair<double>, 只有Pair<Double>, 由于擦除后只有Object, 而Object不能存储double类型.

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

因为存在类型擦除, 因此泛型类型实际上存储的是原始类型. 因此:

if (a instanceof Pair<String>)

是语法错误的.

if (a instanceof Pair<Object>)

也是语法错误的. 由于a被当作Pair类型, 而元素类型被擦除为Object而已, 它自己为一个普通的类, 不存在任何的泛型信息.

因此

if (a instanceof Pair)

是正确的.

同理, 任何Pair的getClass确定都等于Pair.class:

Pair<String> strPair = ...;
Pair<Employee> empPair = ...;
strPair.getClass() == empPair.getClass();

不能建立参数化类型的数组

之因此不能实例化参数类型的数组, 是由于数组会记住它元素的类型, 例如字符串的数组是不能存储浮点数的.

而若是对泛型数组进行实例化, 因为擦除的存在, 致使数组的类型为Object, 则能够存储任何的类型, 这跟数组的语法相冲突, 因此在语法层面上, 参数化类型的数组自己是不容许的. 如:

Pair<String>[] table = new Pair<String>[10]; //ERROR

Varargs警告

假设咱们编写以下的代码:

public static <T> void addAll(Collection<T> coll, T... ts) {
  for (t: ts) coll.add(t);
}
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);

这在语法层面是没有问题的, 运行起来是存在警告的, 是由于虚拟机会创建一个Pair<String>数组, 而这违反了"不能建立参数化类型的数组".

这里之因此正确是由于: 1. 数组的存储空间在编译时期肯定的, 因此须要肯定数组元素的类型. 2. 而针对集合Collection来讲, 它的存储空间是动态递增的, 因此无需考虑元素的类型. 

这能够增长@SafeVarargs来抑制这个警告.

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts){}

不能实例化类型变量

不能使用像new T(...), new T[...]或T.class这样的表达式中的类型变量, 例以下例的Pair<T>构造器是非法的:

public Pair() {first = new T(); second = new T();}

由于类型擦除将T改变为Object, 而new Object()确定不是代码的本意.

同理, 咱们也不能使用:

first = T.class.newInstance();

由于类型擦除的存在, T.class没法明确其Class类型. 因此咱们须要显式的指明其Class类型:

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

咱们能够这样调用:

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

而new T[...]着实让人头疼, 由于类型擦除的缘由致使没法确切知道数组的原始类型(语法层面上数组必须知道其元素类型, 才能判断出String[]存储double时候会报错), 则咱们须要反射的机制(在运行时获取其class的信息, 从而获取其具体的类型, 则可进行new的操做)动态获取其数据类型, 来进行new T[...].

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

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

静态的方法或变量是跟具体的类实例无关的, 而泛型的存在自己就跟具体的类实例有关, 二者冲突致使静态域或方法中引用类型变量是无效的.

public class Singleton<T> {
  private static T singleInstance; //ERROR
  public static T getSingleInstance() {} //ERROR
}

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

由于一旦类型擦除, 根本就不肯定其具体的异常类型.

public static <T extends Throwable> void doWork(Class<T> t) {
  try {

  } catch (Throwable e) { //OK

  } catch (T e) { //ERROR
    
  }
}

因为不能抛出或捕获泛型类, 因此也不能对泛型类进行扩展Exception:

public class Problem<T> extends Exception {} // ERROR

备注: 对"能够消除已检查异常的检查", 不太理解(书章节12.6.7, p543)

擦除后的冲突

例如咱们编写以下的代码:

public class Pair<T> {
  public boolean equals(T value) {return first.equals(value) && second.equals(value);}
}

因为擦除的存在, 致使Pair<String>实际上有两个equals: 

boolean equals(String) //defined in Pair<T>
boolean equals(Object) //inherited from Objects

要么使用"桥方法", 要么重命名函数进行修复.

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

 

泛型类型的继承规则

不管S与T有什么联系(例如子类和父类的关系), Pair<S>和Pair<T>均没有任何关系. 由于Pair<S>和Pair<T>的本质类型都是Pair.

package pair1;


/**
 * Created by lgt on 16/7/9.
 */

class A {
  private String s;
  A(String s) {
    this.s = s;
  }
  public String show() {
    return s;
  }
}
class B extends A {
  B(String s) {
    super(s);
  }
}
public class PairTest1 {
  public static void main(String[] args) {
    Pair<B> b = new Pair<>(new B("hello"), new B("world"));
//    Pair<A> a = b; // ERROR, Pair<B>没法转换为Pair<A>
    Pair c = b;
    c.setFirst(new B("java"));
    System.out.println(((B)c.getFirst()).show());
  }
}

 

通配符类型

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

因此, 若是咱们要打印出全部雇员的信息, 不能定义:

public static void printBuddies(Pair<Employee> p);

而应该定义:

public static void printBuddies(Pair<? extends Employee> p);

备注:

1. 针对语法糖extends, 它每每表示扩展某个接口,类型或者继承了某个类. 例如interface A extends B, 则说明接口A扩展了接口B, class A extends B, 表明A继承B.

因此** A extends B, 则类型为B.

2. 针对A extends B来讲, 只适合get的操做, 由于明确知道其基本类型为B, 但不能执行set操做, 由于不知道具体类型是什么.

通配符的超类型限定

与"? extends Employee"相反, "? super Manager"限制为Manager的全部超类型.

void setFirst(? super Manager);
? extends Employee getFirst();

备注: 针对? super Manager, 只适合set的操做, 由于知道具体类型为Manager, 但不能执行get操做, 由于不知道其基本类型.

无限定通配符

对于Pair<?>的方法:

? getFirst()
void setFirst(?)

getFirst的返回值只能赋给一个Object. setFirst方法不能被调用, 甚至不能用Object调用. Pair<?>和Pair本质的不一样在于: 能够用任意Object对象调用原始的Pair类的setObject方法.

备注: 这里setObject泛指一切set的方法.

因此若是咱们要测试一个Pair是否包含一个null引用, 则能够这样定义:

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

而无需定义成:

public static <T> boolean hasNulls(Pair<T> p){}

一个总结性的例子:

pair3/PairTest3.java:

package pair3;

import pair1.Pair;

/**
 * Created by lgt on 16/7/10.
 */
public class PairTest3 {
  public static void main(String[] args) {
    Manager ceo = new Manager("Gus Greedy", 800000);
    Manager cfo = new Manager("Sid Sneaky", 600000);
    Pair<Manager> buddies = new Pair<>(ceo, cfo);
    printBuddies(buddies);

    ceo.setBonus(1000000);
    cfo.setBonus(500000);
    Manager[] managers = {ceo, cfo};

    Pair<Employee> result = new Pair<>();
    minmaxBonus(managers, result);
    System.out.println("first:" + result.getFirst().getName() + ", second:" + result.getSecond().getName());
    maxminBonus(managers, result);
    System.out.println("first:" + result.getFirst().getName() + ", second:" + result.getSecond().getName());
  }

  public static void printBuddies(Pair<? extends Employee> p) {
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
  }

  public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
    if (a == null || a.length == 0) return;
    Manager min = a[0];
    Manager max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (min.getBonus() > a[i].getBonus()) min = a[i];
      if (max.getBonus() < a[i].getBonus()) max = a[i];
    }
    result.setFirst(min);
    result.setSecond(max);
  }
  public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
    minmaxBonus(a, result);
    PairAlg.swapHelper(result);
  }
}

class PairAlg {
  public static boolean hasNulls(Pair<?> p) {
    return p.getFirst() == null || p.getSecond() == null;
  }
  public static void swap(Pair<?> p) { swapHelper(p);}
  public static <T> void swapHelper(Pair<T> p) {
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
  }
}

pair3/Employee.java:

package pair3;

/**
 * Created by lgt on 16/7/10.
 */
public class Employee {
  private String name;
  private double salary;

  Employee(String n, double s) {
    this.name = n;
    this.salary = s;
  }

  public double getSalary() {
    return salary;
  }

  public String getName() {
    return name;
  }
}

pair3/Manager.java:

package pair3;

/**
 * Created by lgt on 16/7/10.
 */
public class Manager extends Employee {
  private double bonus;
  Manager(String n, double s) {
    super(n, s);
    this.bonus = 0;
  }

  public void setBonus(double b) {
    bonus = b;
  }

  public double getBonus() {
    double baseBonus = super.getSalary();
    return baseBonus + bonus;
  }
}

pair1/Pair.java:

package pair1;

/**
 * Created by lgt on 16/7/9.
 */
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;}
}
相关文章
相关标签/搜索