Java 泛型程序设计

1 定义泛型类和泛型方法

1.1 泛型类

一个泛型类(generic class)即具有一个或多个类型变量的类,在类名后面引入一个<>括起来的类型变量列表后,就可以将类中方法的返回类型、域或局部变量的类型指定为<>中的类型变量。

public class Pair<T,U>
{
	private T first;
	private U second;
	public Pair(T fisrt, U second) {this.first = first; this.second = second;}
	...
}

1.2 泛型方法

在普通类中也可以定义带有类型参数的泛型方法,类型变量放在修饰符之后,返回类型之前,用<>括起。

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

当在调用泛型方法时,在方法名前的<>中放入具体的类型。

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

大多数情况下也可以省略<String>类型参数,编译器可以根据实参列表的类型推断出类型参数的具体类型。但当实参列表中存在不同类型的参数时,编译器可能会提示错误,比如getMiddle(145.06,1,2),编译器会给出错误消息(参数自动装箱为一个Double对象和两个Integer对象,其共同超类为Number和Comparable接口也是泛型类型,这样就无法得到泛型方法的类型变量的具体类型),可以实参全部改为double类型。

2 类型变量的限定与类型擦除机制

2.1 类型变量限定

有时需要对类型变量进行约束,比如某泛型方法中涉及到对类型变量类对象进行比较,需要使实参传进来的对象所属类实现了Comparable接口,可以在定义该泛型方法时,将其类型变量限定为“必须实现了Comparable接口”。

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

这里用extends关键字,其实应该表示了一种“子类型(subtype)”的概念,T的绑定类型可以是类或接口,但是通通使用extends关键字(因为设计者懒得再搞一个新的关键字表示子类型这个概念了)。

2.2 类型擦除

虚拟机中没有泛型类型对象,所有的对象都属于普通类,无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,即擦除了类型变量,将其替换了限定类型(或者Object类)。

比如1.1中的Pair<T,U>在类型擦除后,其类型变量被替换为Object。

public class Pair
{
	private Object first;
	private Object second;
	public Pair(Object fisrt, Object second) {this.first = first; this.second = second;}
	...
}

原始类型用第一个限定的类型变量替换,如果没有给定限定,则用Object替换。

2.2.1 类型擦除中编译器进行的强制类型转换

当程序调用了泛型方法,在虚拟机中擦除了返回类型,那么编译器将插入强制类型转换。

Pair<Employee> buddies = ...;
Employee buddy_first = buddies.getFisrt();

getFirst应该返回Employee类型,但是在虚拟机中将getFirst的返回类型擦除为Object类型了,于是编译器在将buddies.getFirst返回的Object对象赋给buddy_first之前,将其强制转换为Employee类。

2.2.2 编译器生成桥方法避免类型擦除与多态发生冲突

类型擦除和多态很有可能会发生冲突,例如:

public class Child extends Parent<String>
{
	public void hello_world(String v)
	{
		System.out.println("We're in Class Child: " + v);
	}
}

Child继承自类型变量被指定为String的泛型类Parent,在泛型Parent类中实现了对应的泛型函数:

public class Parent <T>
{
	public void hello_world(T v)
	{
		System.out.println("We're in Class Parent: " + v);
	}
}

当企图实现多态时:

public class Main
{
	public static void main(String[] args)
	{
		Child child = new Child();
		Parent<String> parent = child;
		parent.hello_world("Hello World!");
	}
}

这里对于parent.hello_world的调用希望实现多态,调用最合适的方法,而parent引用了Child类的对象,所以应该调用Child.hello_world函数(多态性)。

重点在于:这时在虚拟机中Parent的hello_world(String v)方法经过类型擦除变为了:
public void hello_world(Object v)了,而Child类中并没有重写参数类型为Object的hello_world方法,但是实际上却可以运行成功。

这是因为编译器在Child类中生成了一个桥方法(bridge method):

public void hello_world(Object v)
{
	hello_world((String) v); // 强制类型转换,调用Child类中参数类型为String的hello_world方法
}

这个桥方法实际上就相当于Child类对于Parent中hello_world(Object v)的重写。而Child类中的hello_world(String v)本意上是对超类中hello_world的重写,但是由于发生了类型擦除,导致重写失败,即类型擦除与多态性之间发生了冲突,桥方法就是编译器为了解决这种冲突而添加的机制。

3 Java泛型中存在的约束与局限性

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

比如,对于泛型类Pair<T>,不能将其实例化为Pair<int>,应该实例化为Pair<Integer>。这一点限制是类型擦除产生的,发生类型擦除之后,Pair类中使用Object类型的域存储值,而Object无法存储一个基本类型的值。

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

当使用反射机制查询运行时对象的类类型时,无法获得具有特定类型变量的类信息,比如:

public class Main
{
	public static void main(String[] args)
	{
		Pair<String> stringPair = ...;
		Pair<Employee> employeePair = ...;
		if (stringPair.getClass() == employeePair.getClass())
		// true
		{
			...
		}
	}
}

这里stringPair参数类型为String,而employeePair参数类型为Employee,但是对它们getClass都将返回Pair.class。

3.3 不能创建参数化类型的数组

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

创建参数化类型的数组的不允许的,这一点限制也是由于类型参擦而导致的。

首先来看一下ArrayStoreException:

Pair[] table = new Pair[10];
table[0] = new Object(); 
// 编译时出错,不能其他类型元素
Pair[] table = new Pair[10];
Object[] o = table; 
o[0] = new Object();
// 编译时不会出错,但是运行时会抛出ArrayStoreException异常

也就是说,数组存储时只能存储和其创建时元素类型相同的元素,而类型擦除机制可能会破坏这一规则
然后再试图再objarray中存储其他非Pair类型的元素,会抛出ArrayStoreException异常,而擦除会使这种机制失效:

Pair<String>[] table = new Pair<String>[10]; // 假设合法
Object[] objarray = table;
objarray[0] = new Pair<Double>;

由于发生了类型擦除,table中的类型变量被擦除为Object,这样会导致Pair<Double>类型的值存储进应该存储Pair<String>的数组中,这破坏了上面的数组存储规则,所以,不能创建参数化类型的数组。

但是,只是不允许创建这种数组,却可以声明类型为Pair<String>[]的数组,只是不能new这样的数组。另外,创建通配符的泛型数组是允许的:

Pair<?>[] table = new Pair<?>[10];

或者,可以声明参数化类型数组,在创建时new不具有参数化类型的数组:

Pair<String>[] table = new Pair[10];
ArrayList<String> list = new ArrayList();

3.4 Varargs警告

有一种情况可能会违反上面的“不能创建参数化类型数组”的规则,即向参数个数可变的方法传递泛型类型实例:

public static<T> void addAll(Collection<T> coll, T...ts)
{
	for (t : ts) coll.add(t);
}

这里实际上参数ts是一个数组,包含了所有提供的实参,当调用该方法,且具有多个T类型实参时,Java虚拟机将必须建立一个参数化类型数组,比如:

Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);

这里,Java虚拟机建立了一个Pair<String>数组,这违反了前面的规则,但是对于这种情况,规则有所放松,不会得到错误信息,而是得到警告。

通过通过在语句或者整个函数前加上:

@SuppressWarnings("unchecked")
@SafeVarargs

即可抑制这种警告。

3.5 不能实例化类型变量

不能使用类似于new T(…), new T[…]或T.class这种表达式,比如:

public class Pair<T>
{
	private T first;
	private T second;
	public Pair()
	{
		// wrong
		first = new T();
		second = new T(); 
	}

类型擦除导致T变为Object,而new Object就非常不可取。

解决方法是,让调用者提供一个构造器表达式:

public static <T> Pair<T> makePair(Supplier<T> constr) // Supplier<T> 是一个函数式接口,表示一个无参数且返回类型为T的函数
{
	return new Pair<>(constr.get(), constr.get());
}
Pair<String> p = Pair.makePair(String::new); // String::new即lambda表达式中方法引用的构造器引用,作为一个函数传递好函数式接口Supplier<T>

或者,在调用MakePair函数时提供类型实例的Class信息:

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);

3.6 不能构造泛型数组

与不能够实例化一个泛型实例一下,也不能实例化数组。虽然数组会填充null值,构造时看上去是安全的,但是数组本身也有类型,这个类型会被擦除,比如:

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

类型擦除导致这个方法永远在构造Comparable[2]数组。

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

不能再静态域或方法中引用类型变量,比如:

public class Singleton<T>
{
	private static T singleInstance; // wrong
	public static T getSingleInstance() // wrong
	{
		if(singleInstance == null)
			...
		return singleInstance;
	}
}

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

既不能抛出也不能捕获泛型类对象,泛型类扩展Throwable也是不合法的。
比如:

public class Problem<T> extends Exception {...} // wrong

在catch子句中也不能使用类型变量,但是在异常规范中使用类型变量是允许的。

3.9 类型擦除后的冲突

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

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

Comparable<Employee>和C

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值