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

泛型程序设计

泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。一个 ArrayList 类可以聚集任何类型的对象,这是一个泛型程序设计的实例。

类型参数的好处

ArrayList<String> files = new ArrayList<>();
  • 当调用 get 的时候,不需要进行强制类型转换,编译器就知道返回值类型为 String:
    • String filename = files.get(0);
  • 编译器还知道 ArrayList 中 add 方法有一个类型为 String 的参数。可以避免插入错误类型的对象。
    • files.add(new File("…")); //error
类型参数的魅力在于:使得程序具有更好的可读性和安全性。

实现泛型类

对于类型参数,需要内置所有的类,需要预测出所有类的未来可能有的所有用途。
ArrayList 类有一个方法 addAll 用来添加另一个集合的全部元素。

定义泛型类

public class Name<T,U> {private T Name1; private U Name2;} // 第一个域和第二个域可以使用不同的类型

泛型方法

// 普通类的泛型方法
class ArrayAlg{public static <T> T getMiddle(T... a) {return a[a.length / 2];}}
---
// 调用泛型方法,一般情况下可以省略<String>(可推导的情况下)
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
// 在不指定type的情况下,参数的类型如果不一致,则将寻找共同超类型。失败则报错。

类型变量的限定

// 将T限制为实现了Comparable接口,也可以用 extends 限定继承超类。
public static <T extends Comparable> T min(T[] a)...
// 用“&”加入多个限定,但至多只有一个类(单继承特性),且作为第一位。 

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用 Object)。
public class Interval<T extend Comparable & Serializable>{...}
public class Interval;// T原始类型为Comparable。遇到Serializable的属性时,编译器会在必要时插入强制类型转换。
为提高效率,应该将标签接口(没有方法的接口)放到边界列表的末尾。

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
编译器将泛型方法调用翻译为两条指令:
  • 对原始方法的调用
  • 将返回类型强制转化为变量类型

翻译泛型方法
  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。
问题1:
public class pair {...; public void setSecond(Object second) {this.second=second;}} 
// pair类型擦除后得到的方法
// public calss pair<T> {...; public void setSecond(T second) {this.second=second;}}
// DateInterval继承pair后,继承了pair的setSecond方法,在调用时会发生冲突。
class DateInterval extends pair {
public void setSecond(LocalDate second) {...;}}
DateInterval interval = new DateInterval(...); // 创建对象
Pair<LocalDate> pair = interval; // 类型转换
pair.setSecond(aDate); // 传入的 Date 参数也能算作 Object 参数
当子类对象被泛型父类调用时,类型擦除将会与多态发生冲突。要解决这个问题,需要在子类中生成一个桥方法覆盖原父类方法:public void setSecond(Object second) {setSecond((Date) second)};
问题2:
若 DateInterval 方法也覆盖了 getSecond 方法,则在此类中将出现两个返回值不同 getSecond 方法。
但在虚拟机中,用参数类型和返回类型确定一个方法,这个问题能够得到解决。
在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。原方法与覆盖方法称为具有协变的返回的类型。合成的桥方法调用了新定义的方法。

约束与局限性
  • 不能用基本类型实例化类型参数
  • 运行时类型查询只适用于原始类型
  • 不能创建参数化类型的数组
    • Pair<String>[] = ...; // OK
      ... = new Pair<String>[10]; // ERROR
      
    • ArrayList<Pair<String>> // 创建参数化类型数组的唯一方法
      
  • Varargs警告
    • public static <T> void addAll(Collection<T> coll, T...ts){} // 参数个数可变的方法
      // 参数被收集至Pair<String>数组,这违反了第三条规则,但不会抛出异常,只会发出警告
      
    • 抑制警告的方法:
      • 为包含 addAll 调用的方法增加注解 @SuppressWarnings(“unchecked”)
      • 直接用 @SafeVarargs 标注 addAll 方法。对于只需要读取参数数组元素的所有方法,都可以使用这个注解
  • 不能实例化类型变量
public Pair() { first = new T(); second = new T();} //ERROR
// 解决办法1:借用函数式接口
Pair<String> p = Pair.makePair(String::new);// 提供构造器表达式
--- // makePair接收一个Supplier<T>, 这是一个函数式接口,表示一个无参数而且范湖类型为T的函数
public static <T> Pair<T> makePair(Supplier<T> constr) 
{ return new Pair<>(constr.get(), constr.get()); }
// 解决办法2:反射调用 Class.newInstance
public static<T> Pair<T> makePair(Class<T> cl)
{
    try { return new Pair<>(cl.newInstance(), cl.newInstance()); }
    catch (Exception ex) { return null;}
}
--- // 调用
Pair<String> p = pair.makePair(String.class);
  • 不能构造泛型数组
public static <T extends Comparable> T[] minmax(T[] a) { T[] mm = new T[2];...} // ERROR
// 解决办法1:提供一个数组构造器表达式
String[] ss = Array.minmax(String[]:new, "Tom", "Dick", "Harry");
// String:new 指示一个函数,给定所需的长度,会构造一个指定长度的 String 数组
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
{ T[] mm = constr.apply(2);}
// 解决办法2:利用反射
public static <T extends Comparable> T[] minmax(T... a)
{ T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(), 2); ...}

  • 泛型类的静态上下文中类型变量无效
  • 不能抛出或捕获泛型类的实例
public static <T extends Throwable> void doWork(Class<T> t){
try {...}
catch(T e) { // ERROR
...}}

//解决办法:
public static <T extends Throwable> void doWork(T t) throws T // OK
{
	try{}
	catch (Throwable realCause){
		t.initCause(realCause);
		throw t;
	}
}

  • 可以消除对受查异常的检查
@SuppressWarning("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throw T { throw (T) e;}

假设该方法包含于 Block 中,通过调用该类静态方法即可将异常包装为非受查异常。
try {do work} catch (Throwable t) {Block.<RuntimeException>throwAs(t);}

将以上代码包装在一个抽象类。用户可以覆盖 body 方法来提供一个具体的动作。调用 toThread 时,会得到 Thread 类的一个对象,它的 run 方法不会介意受查异常。
public abstract class Block {
	public abstract void body() throws Exception;
	public Thread toThread(){
		return new toThread(){
			public void run(){
				try{
					body();
				}catch (Throwable t){
					Block.<RuntimeException>throwAs(t);
				}}};}
    @SuppressWarning("uncheck")
    public static <T extends Throwable> void throwAs(Throwable e) throws T {
        throw (T) e;
    }
    public static void main(String[] args) {
        new Block(){
            public void body() throws Exception{
                Scanner in = new Scanner(new File("ququx"), "UTF-8");
                while (in.hasNext())
                    System.out.println(in.next());
            }}.toThread().start();}}

运行单元测试时,会得到一个栈轨迹,其中包含一个 FileNotFoundException。
正常情况下,你必须捕获线程 run 方法中的所有受查异常,把它们包装到非受查异常中,因为 run 方法声明不抛出任何受查异常。
注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。
例如:一个泛型类擦除前为T,此时他与父类具有两个同名不同参的方法。
boolean equals(T); // 内部定义
boolean equals(Object); // 父类继承
// 参数不同未覆盖

擦除后 T 被限定为 Object,此时,两个方法就变成同名同参的方法,无法正确调用。
解决办法就是重新命名引发错误的方法。
一个类或类型变量不能同时成为两个接口类型的子类,因为这两个接口时同一接口的不同参数化。
// 非泛型版本合法
class Employee implement Comparable<Employee> {...}
class Manager extends Employee implements Comparable<Manager> {...} // ERROR


泛型类型的继承规则

无论 S 与 T 有什么联系,通常 Pair 与 Pair 没什么联系。对于类型安全非常重要。将Pair赋给Pair不合法。
数组容许将子类对象赋给父类变量,但此时试图将父类对象加入数组时,将会报错。
在将参数化类型转换为一个原始类型后,给参数化域赋值时会抛出异常。

通配符

通配符类型中,允许类型参数变化
Pair<? extends Employee> // 表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类

// public static void printBuddies(Pair<Employee> p) 不能将 Pair<Manager> 传递给这个方法
public static void printBuddies(Pair<? extends Employee> p) // OK
? extends Employee getFirst();
void setFirst(? extends Employee);

// wildcardBuddies 不接受传递任何特定类型,故无法确定 ? 是什么类型,一父对多子继承
// First 位于 Pair<T> 的域中,类型为 T
Pair<Manager> managerBuddies = new Pair<>(...);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.getFirst(); // OK
wildcardBuddies.setFirst(lowlyEmployee); // ERROR 


超类型限定

Pair<? super Manager> // 这个通配符限制为Manager的所有超类型。

// First 位于 Pair<T> 的域中,类型为 T
void setFirst(? super Manager); //setFirst具体类型未知,只能传递 Manager 对象,或者某个子类型。
? super Manager getFirst(); // 无法保证返回对象的类型,只能把它赋给一个 Object。

带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
public interface Comparable<T> { public int compareTo(T other); } //给定一个泛型接口,内有方法compareTo

// 以下方法存在一个问题,当 T 实现了抽象类 superT,superT 拓展了 Comparable<superT> 接口,
// 那么 T 实现的是 Comparable<superT> 而不是 Comparable<T>
public static <T extends Comparable<T>> T min(T[] a) // 此时 T 是 Comparable<T> 的子类型

// 用超类型限定救助:
public static <T extends Comparable<? super T>> T min(T[] a)...
// compareTo 方法变为:
int compareTo(? super T)
// 有可能被声明为使用类型 T 的对象,也有可能使用 T 的超类型。


无限定通配符

Pair<?> // 无限定通配符
? getFirst() // 结果只能赋给 Object 类
void setFirst() // 不能调用,只能 setFirst(null)
// 与 Pair 的差别:可以用任意 Object 对象调用原始 Pair 类的 setFirst 方法

其对于许多简单的操作非常有用,当操作不需要实际的类型时(判空)。

通配符捕获

通配符不是类型变量,不能使用 ?作为一种类型。
public static <T> void swapHelper(Pair<T> p) // 泛型方法
public static void swap(Pair<?> p) // 非泛型
{ swapHelper(p);} // swapHelper 方法的参数 T 将捕获通配符,只有在 T 指出类型时才有明确的含义。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值