Java在5.0开始支持范型,能做到更好的类型安全性和可读性,减少容器的元素类型与Object的强制转换。不过,与C++相比,Java的范型相对比较弱,因为范型类的不同类型参数并不会真正实例化出独立的类型,而是通过类型擦除的技术来实现范型。
范型基本用法
范型类
范型类基本形式:class ClassName<T,U>
一个简单的例子:
class MyArray<E> {
MyArray() {
}
public int size() { return size; }
public boolean isEmpty() {
return size == 0;
}
public void add( E item) {
grow(size + 1);
items[size++] = item;
}
public E get(int index) {
if ( index >= size ) {
throw new IndexOutOfBoundsException("index out of bounds");
}
return (E)items[index];
}
private void grow(int minCapacity) {
int oldCapacity = items.length;
if ( oldCapacity >= minCapacity ) {
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
items = Arrays.copyOf(items, newCapacity);
}
private Object[] items = {};
private int size = 0;
}
// test
MyArray<String> ma = new MyArray<>();
ma.add("hello");
ma.add("world");
for ( int i=0; i<ma.size(); ++i ) {
System.out.println( ma.get(i));
}
范型方法
可以定义带有参数类型的方法,类型变量<T>
需要放在修饰符(public static)的后面,返回类型的前面
class ArrayAlg {
public static <T> T getMiddle(T[] a) {
return a[a.length/2];
}
}
编译器能自动推断出参数的类型,实际函数调用时,不用传递类型变量,例如:
String[] names = {"hello", "world"};
ArrayAlg.getMiddle(names);
限定符 extends
可以对类型参数T设置依赖:<T extends BoundingType1 & BoundingType2, U>
,T和BoundingType可以是类型活着接口,T要求是BoundingType1和BoundingType2的子类型
限定类型用&
分隔,逗号用来分隔类型变量
例如:
T 需要实现了Comparable接口,才能调用compareTo方法。
public static <T extends Comparable> T min(T[] a) {
if ( a==null || a.length == 0 ) {
return null;
}
T smallest = a[0];
for (int i=0; i<a.length; ++i) {
if (smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
范型的实现机制(类型擦除)
类型擦除:Java虚拟机没有范型类型对象,所有范型类型的定义都自动提供了一个相应的原始类型(raw type),也就是删除了类型参数后的范型类型名,同时擦除(erased)类型变量,并替换为第一个限定类型(没有限定类型的变量用Object)。
范型类的翻译
class MyClsTest<T extends Comparable, U> {
T t;
U u;
}
// 编译器翻译
class MyClsTest {
Comparable t;
Object u;
}
范型表达式的翻译
调用范型方法,如果擦除返回类型,编译器会插入强制转换
Pair<Employee> p = new Pair<Employee>();
Employee e = p.getFirst();
// 编译器翻译
Pair p = new Pair();
Employee e = (Employee)p.getFirst();
范型方法的翻译
public static <T extends Comparable> T min(T[] a);
// 编译器翻译
public static Comparable min(Comparable[] a);
范型的约束限制
不能使用基本类型实例化类型参数
因为类型擦除的存在,类型参数只支持对象类型,比如Pair<double>
不合法
instanceof类型查询只适用于原始类型
JVM中对象没有范型,instanceof检测类型,查的就是原始类型,比如 if(a instanceof Pair<String>)
等价于if(a instanceof Pair)
不支持参数化类型的数组
比如:Pair<String>[] table = new Pair<String>[10];
不支持
因为数组不像范型容器,它知道自己集合的类型,如果支持范型类型的数组,因为类型擦除的存在,会导致类型错误。
不能直接实例化类型变量,需要借助于 Class<T>
- 不能使用类似
new T()
,T.class
之类的表达式 - 类型变量的实例化需要借助于
Class<T>
例如:
class Pair<T> {
public Pair() {}
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; }
private T first;
private T second;
}
private static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<T>( cl.newInstance(), cl.newInstance() );
} catch (Exception e) {
return null;
}
}
// test
Pair<String> pr = makePair(String.class);
pr.setFirst("hello");
不能实例化类型变量的数组,需要借助于 Array.newInstance
例子:
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mm = (T[])Array.newInstance( a.getClass().getComponentType(), 2 );
// ...
}
不能在静态变量或方法中引用范型类的类型变量
class MyClass<T> {
private static T instance; // ERROR
}
通配符 ?
Java支持使用?
通配符类型来支持类型参数变量的继承关系
extends 子类限定
例如:Pair<? extends Employee>
表示参数类型是Employee的子类
public static void print( Pair<? extends Employee> p ) {
Employee e = p.getFirst();
// p.setFirst( obj );
}
print方法可以接受Pair<Employee>
或Pair<Manager>
,如果Manager继承自Employee
注意Pair<? extends Employee>
的限定对象的特点:
setFirst和getFirst的方法:
? extends Employee getFirst();
void setFirst(? extends Employee);
- 可以返回值,如getFirst(),因为编译器知道返回对象是继承自Employee的,因此是合法的
- 不能提供参数,如setFirst,编译器无法明确传入的参数类型,因此拒绝传递任何特定的类型
- 带有子类限定的通配符,可以从范型对象读取
super 超类限定
? super Manager
的行为与extends相反,表示参数是Manager的超类,可能是Object
注意Pair<? super Manager>
的限定对象的特点:
? super Manager getFirst();
void setFirst(? super Manager);
- 因为getFirst不确定类型,可能是Object,因此只能用Object来接
- 可以为方法提供参数,如setFirst支持写入Manager及其超类的对象
- 带有超类限定的通配符,可以向范型对象写入
参考
- 《Java核心技术 卷1》
- https://itimetraveler.github.io