本文由 ImportNew - 陈 晓舜 翻译自 javaworld。欢迎加入Java小组。转载请参见文章末尾的要求。
参数化类型是一种泛型类型实例,泛型类型的类型参数被真实的类型参数(参数名称)替换。例如:Set<String> 是参数化类型,其中真正类型参数String替换类型参数E。
Java语言支持下面几种真正类型参数:
- 实体类型:传入一个类或其他引用类型名称作为类型参数。例如,List,Animal作为参数传给E。
- 实体参数化类型:传入一个实体参数化类型名称作为类型参数。例如,Set<List>,List作为参数传给E。
- 数组类型:传入一个数组作为类型参数。例如,Map<String, String[]>,String传入给K,String[]传入给V。
- 类型参数:直接把类型参数传入作为类型参数。例如,在类Container {Set elements;}中,E就作为参数传给了E。
- 通配符:传入问号符作为类型参数。例如,Class<?>,?号作为参数类型传给T。
每一个泛型类型都有原生类型的存在,即不包含形参类型列表的泛型类型,例如,Class就是Class的原生类型。跟其他泛型类型不一样,原生类型可以用于任何类型的对象。
定义和使用泛型类型
定义一个泛型类型需要指定形参列表并在它的实现中贯穿使用这些参数。使用泛型则需要在初始化时传入真正的类型参数给形参。看一下清单5:
Listing 5. GenDemo.java (version 1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | class Container<E>
{
private E[] elements;
private int index;
Container( int size)
{
elements = (E[]) new Object[size];
index = 0 ;
}
void add(E element)
{
elements[index++] = element;
}
E get( int index)
{
return elements[index];
}
int size()
{
return index;
}
}
public class GenDemo
{
public static void main(String[] args)
{
Container<String> con = new Container<String>( 5 );
con.add(“North”);
con.add(“South”);
con.add(“East”);
con.add(“West”);
for ( int i = 0 ; i < con.size(); i++)
System.out.println(con.get(i));
}
}
|
清单5展示了泛型的定义和保存合适参数类型的简单Container 类型的使用。为了使代码简单点,我省略了一些错误检查代码。
Container 类通过指定形参类型列表把它自己定义为泛型。类型参数E用于指定保存被添加到内部数组的元素和取出元素时返回的类型。
Container(int size) 构造函数通过elements = (E[]) new Object[size]; 创建数组。如果你奇怪我为什么不指定elements = new E[size]; ,因为做不到啊:如果我们那样定义,会导致ClassCastException 。
编译清单5(javac GenDemo.java )。E[]转换会导致编译器输出转换未被检查的警告。这标示着从Object[]向下转型为E[]可能会导致类型安全问题,因为Object[]可以保存任何类型的对象。
注意,尽管在这个例子中不会造成类型安全问题。在内部数组中不可能保存非E的对象。我会在将来的文章告诉你怎么去掉这个警告信息。
执行java GenDemo 运行这个程序。你可以看到下面的输出:
类型参数界限
Set是一个未绑定类型参数的例子,因为你可以传入任何实际的参数类型给E。例如,你可以指定Set<Marble> ,Set<Employee> 或Set<String> 。
有时,你希望可以限制传入给类型参数的实际类型参数的类型。例如,你可以希望限制类型参数只接受Employee 和它的子类。
你可以通过指定上界来限制类型参数,这是一个传入实际类型参数的最高限制。通过预留关键字extends 后跟上限类型名称来指定上限类型。
例如,Employees<E extends Employee> 类限制了传入给Employees 的类型必须为Employee 或子类(例如,Accountant )。指定new Employees<Accountant> 是可以的,但new Employees<String> 就不行了。
你可以给类型参数指定多个上界。然后,第一个限定必须为一个类,其他的限定必须为接口。每一个限定是通过& 符来进行分割的。我们看一下清单6。
Listing 6. GenDemo.java (version 2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import java.math.BigDecimal;
import java.util.Arrays;
abstract class Employee
{
private BigDecimal hourlySalary;
private String name;
Employee(String name, BigDecimal hourlySalary)
{
this .name = name;
this .hourlySalary = hourlySalary;
}
public BigDecimal getHourlySalary()
{
return hourlySalary;
}
public String getName()
{
return name;
}
public String toString()
{
return name+”: “+hourlySalary.toString();
}
}
class Accountant extends Employee implements Comparable<Accountant>
{
Accountant(String name, BigDecimal hourlySalary)
{
super (name, hourlySalary);
}
public int compareTo(Accountant acct)
{
return getHourlySalary().compareTo(acct.getHourlySalary());
}
}
class SortedEmployees<E extends Employee & Comparable<E>>
{
private E[] employees;
private int index;
SortedEmployees( int size)
{
employees = (E[]) new Employee[size];
int index = 0 ;
}
void add(E emp)
{
employees[index++] = emp;
Arrays.sort(employees, 0 , index);
}
E get( int index)
{
return employees[index];
}
int size()
{
return index;
}
}
public class GenDemo
{
public static void main(String[] args)
{
SortedEmployees<Accountant> se = new
SortedEmployees<Accountant>( 10 );
se.add( new Accountant(“John Doe”, new BigDecimal(“ 35.40 ”)));
se.add( new Accountant(“George Smith”, new BigDecimal(“ 15.20 ”)));
se.add( new Accountant(“Jane Jones”, new BigDecimal(“ 25.60 ”)));
for ( int i = 0 ; i < se.size(); i++)
System.out.println(se.get(i));
}
}
|
清单6的Employee类抽象出了领时薪的雇员概念。Accountant 是它的子类,并且实现Comparable<Accountant> 表明Accountants 可以根据自然顺序排序,在这个例子中是通过时薪。
java.lang.Comparable 接口被定义为接收一个类型参数T的泛型类型。这个类提供了一个int compareTo(T o) 方法用于比较当前对象和传入参数(T类型),当当前对象小于,等于,大于指定对象时分别返回负整数,0,和正整数。
SortedEmployees 类在内部数组中允许你保存继承Employee且实现Comparable 的实例。这个数组会在Employee 子对象被添加后根据时薪进行顺序排序(通过java.util.Arrays 的void sort(Object[] a, int fromIndex, int toIndex) 类方法)。
编译清单6(javac GenDemo.java )并运行(java GenDemo )。你应该可以看到下面的输出:
1 2 3 | George Smith: 15.20
Jane Jones: 25.60
John Doe: 35.40
|
那下界呢?
你不能指定为一个泛型类型参数指定一个下限限制,想要知道为什么的我推荐阅读Angelika Langer的Java泛型关于下限限制的FAQs,但她说“会比较难理解并且没有什么用”。
说说通配符
我们来看看,如果你想要打印出对象列表,不管这个对象是strings ,employees ,shapres 还是一些其他的类型。你首先要做的应该是类似清单7。
Listing 7. GenDemo.java (version 3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenDemo
{
public static void main(String[] args)
{
List<String> directions = new ArrayList<String>();
directions.add(“north”);
directions.add(“south”);
directions.add(“east”);
directions.add(“west”);
printList(directions);
List<Integer> grades = new ArrayList<Integer>();
grades.add( new Integer( 98 ));
grades.add( new Integer( 63 ));
grades.add( new Integer( 87 ));
printList(grades);
}
static void printList(List<Object> list)
{
Iterator<Object> iter = list.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}
}
|
strings 和integers 的列表是objects 表明表的子类型,看起来很符合逻辑。但当你尝试去编译时,编译器会报错。明确地告诉你string 列表不能转换为object 列表,integer 列表也一样。
你看到的错误信息跟泛型的基本规则有关。
对于一个指定的类型y的子类x和一个原生类型的定义G,G<x> 不是G<y> 的子类型。根据这条规则,尽管String 和java.lang.Integer 是java.lang.Object 的子类型,List<String> 和List<Integer> 却不是List<Object> 的子类型。
为什么会有这样一条规则?还记得吗,泛型是为了在编译时捕获类型安全错误才被设计出来的,这可是很有用的:没有泛型时,你有可能会在半夜两点被叫起去工作,就是因为你的Java程序抛出了一个ClassCastException 然后崩溃了。
作为展示,我们假设List<String> 是List<Object> 类型的子类。如果这成立,你可以写下面的代码:
1 2 3 4 | List<String> directions = new ArrayList<String>();
List<Object> objects = directions;
objects.add( new Integer());
String s = objects.get( 0 );
|
这个代码段创建了一个基于array list的strings列表。之后把它转换为objects列表(这是不可行的,但现在我们先假设它是成立的)。接着添加一个会引起类型安全问题的integer 到objects列表中。问题就出在最后一行,因为保存的integer 不能被转换为string ,所以会抛出ClassCastException 。
没有泛型,在清单7中你唯一的避免这种类型安全问题的选择就是传一个类型为List<Object> 的对象给printList() 方法,但这用处并不大。有了泛型,你可以通过通配符来解决这个问题,如清单8所示。
Listing 8. GenDemo.java (version 4)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenDemo
{
public static void main(String[] args)
{
List<String> directions = new ArrayList<String>();
directions.add(“north”);
directions.add(“south”);
directions.add(“east”);
directions.add(“west”);
printList(directions);
List<Integer> grades = new ArrayList<Integer>();
grades.add( new Integer( 98 ));
grades.add( new Integer( 63 ));
grades.add( new Integer( 87 ));
printList(grades);
}
static void printList(List<?> list)
{
Iterator<?> iter = list.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}
}
|
清单8中我使用了通配符(?标记)代替了在printList() 的参数list 和方法体中的Object 。因为这个符号代表任意类型,传List<String> 和List<Integer> 给这个方法都是合法的。
编译清单8(javac GenDemo.java)并运行程序(java GenDemo)。你应该可以看到下面的输出:
探索泛型方法
现在假设你想要复制一个objects列表中满足某些filter条件的元素到另外一个list。你也许会想到定义一个方法void copy(List<Object> src, List<Object> dst, Filter filter) ,但这个方法只能用于复制Objects列表,其他的根本不行。
如果你想要传入任意类型的list给源list和目标list,你需要使用通配符作为一个类型占位符。例如,看看下面的copy() 方法:
1 2 3 4 5 6 | void copy(List<?> src, List<?> dest, Filter filter)
{
for ( int i = 0 ; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}
|
这个方法的参数list是正确的,但有个问题。编译器报告dest.add(src.get(i)); 触发了类型安全问题。?表明任何类型的对象都可以是list的对象类型,有可能源类型和目标类型并不兼容。
例如,如果源列表是Shape类型的List,而目标列表是String类型的List,copy方法是可以正常执行的,但当尝试去获取目标列表的元素时就会抛出ClassCaseException 。
你可以使用通配符的上界和下界来部分解决这个问题,如下:
1 2 3 4 5 6 7 | void copy(List<? extends String> src, List<? super String> dest,
Filter filter)
{
for ( int i = 0 ; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}
|
|