- 一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以适用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
- 在面向对象编程语言中,多态是一种泛化机制。
- JavaSE5 的重大变化之一: 泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。泛型 这个术语的意思是 : 适用于许多许多的类型。
- 如果你了解其他语言(例如C++) 中的参数化类型机制,你就会发现,有些以前能够做到的事情,使用Java泛型机制缺无法做到。使用别人构建好的泛型类型相当容易。但是如果你要自己创建一个泛型实例,就会遇到很多令人吃惊的事情。
- 这并非说Java泛型毫无用处。在很多情况下,它可以使代码更直接更优雅。不过,瑞国你具备其他语言的经验,而那种语言实现了更纯粹的泛型,那么,Java可能令你失望了。
Java泛型与C++的比较
-
Java的设计者曾说过,设计这门语言的灵感主要来自C++。
-
了解C++模板的某些方面,有助于你理解泛型的基础。同时,非常重要的一点是,你可以了解Java泛型的局限是什么,以及为什么会有这些限制。最终目的是帮助你理解,Java泛型的边界在哪里。因为你只有知道某个技术不能做到什么,你这样才能更好地做到所能做的。
-
Java社区中,人们普遍对C++模板有了一种误解,而这种误解可能会误导你,令你在理解泛型的意图时产生偏差。
简单泛型
- 有很多原因促成了泛型的出现,最引人注目的一个原因是,就是为了创造容器类。容器,就是存放要使用的对象的地方。数组也是如此,不过与简单的数组相比,容器更加灵活,具备更多不同的功能。
//一个只能持有单个对象的类,这个类可以明确指定其持有的对象的类型 ,如下例子
public class Automobile {}
class Holder1{
private Automobile automobile;
public Holder1(Automobile automobile) {
this.automobile = automobile;
}
Automobile getAutomobile(){
return automobile;
}
}
- 这个类有以下问题 : ①可重用性不好 ②无法持有其他类型的任何对象
//JavaSE 5 之前,我们可以在类中直接持有Object类型的对象
public class Holder2 {
private Object object;
public Holder2(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
public static void main(String[] args) {
Holder2 holder2 = new Holder2(new Automobile());
//把object 强制转型为 Automobile
Automobile automobile = (Automobile) holder2.getObject();
//设置对象的属性
holder2.setObject("the object is Automobile");
//获取对象的属性
String string = (String) holder2.getObject();
holder2.setObject(1);
Integer a= (Integer) holder2.getObject();
}
}
- Holder2 可以存储任何类型的对象,在这个例子中,只用了一个Holder2对象,却先后存储了三种不同类型的对象 (Automobile , String ,Integer 三个对象)。
- 有些时候,我们确实希望容器能够同时持有多种类型的对象。但是,通常而言,我们只会使用容器来存储一种类型的对象。泛型的主要目的之一就是指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
public class Holder3 <T>{
private T t;
public Holder3(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Holder3<Automobile> holder3 = new Holder3<Automobile>(new Automobile());
Automobile automobile = holder3.getT();
//如下会报错
//holder3.setT(1); setT(参数必须为 Automobile 类型)
}
}
- 当你创建Holder3 对象时,必须指明有什么类型的对象,将其置于尖括号内。就像 main() 中那样。然后,你就只能在 Holder3 中存入该类型(或其子类,因为多态与泛型不冲突)的对象了。
- 在你从 Holder3 中取出它持有的对象时,自动地就是正确的类型。
- 这就是Java 泛型的核心概念: 告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
一个元组类库
- 仅一次方法调用就能返回多个对象,你应该经常需要这样的功能吧。可是 return 语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。
- 当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。可是有了泛型,我们就能够一次性地解决该问题,以后再也不用在这个问题上浪费时间了。同时,我们再编译期就能确保类型安全。这个概念被称为 元组。
- 元组: 它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也被称为 数据传送对象或信使)
//如下 是一个2维元组,它能够持有两个东西
public class TwoTuple<A,B> {
public final A a;
public final B b;
public TwoTuple(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public String toString() {
return "TwoTuple{" +
"a=" + a +
", b=" + b +
'}';
}
}
- 构造器捕获了要存储对象,而toString() 是一个便利函数,用来显示列表中的值。注意,元组隐含地保持了其中元素的次序。
- 问题发现: 第一次阅读上面的代码时,你也许会想,这不是违反了 Java编程的安全性原则吗?
- a 和 b 应该声明为 private ,然后提供 getA() 和 getB() 之类的访问方法才对呀? 让我们仔细看看这个例子中的安全性: 客户端程序可以读取 a 和 b 对象,然后可以随心所欲地使用 这两个东西。但是 ,它们却无法将其他赋值 赋予 a和 b 。因为 final 声明 为你买了相同的安全保险,而且这种格式更简洁明了。
- 还有另一种设计考虑,即你确实希望允许客户端程序员改变 a 和 b 所引用的对象。然而, 采用以上的形式无疑是更安全的做法,这样的话,如果程序员想要使用具体具有不同元素的元组,就强制要求他创建一个新的 TwoTuple 对象。
//我们可以利用继承实现长度更长的元组
public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {
public final C c;
public ThreeTuple(A a, B b, C c) {
super(a, b);
this.c = c;
}
@Override
public String toString() {
return "ThreeTuple{" +
"c=" + c +
", a=" + a +
", b=" + b +
'}';
}
}
class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> {
public final D d;
public FourTuple(A a, B b, C c, D d) {
super(a, b, c);
this.d = d;
}
@Override
public String toString() {
return "FourTuple{" +
"c=" + c +
", d=" + d +
", a=" + a +
", b=" + b +
'}';
}
}
class FiveTuple<A, B, C, D, E> extends FourTuple<A, B, C, D> {
public final E e;
public FiveTuple(A a, B b, C c, D d, E e) {
super(a, b, c, d);
this.e = e;
}
@Override
public String toString() {
return "FiveTuple{" +
"c=" + c +
", d=" + d +
", e=" + e +
", a=" + a +
", b=" + b +
'}';
}
}
- 为了使用元组,你只需定义一个长度适合的元组,将其作为方法的返回值,然后在 return 语句中创建该元组,并返回即可。
class Amphibian {}
class Vehicle {}
class TupleTest {
static TwoTuple<String, Integer> f() {
return new TwoTuple<>("hi", 47);
}
static ThreeTuple<Amphibian,String,Integer> g(){
return new ThreeTuple<>(new Amphibian(),"hi",47);
}
static FourTuple<Amphibian,String,Integer,Double> k(){
return new FourTuple<>(new Amphibian(),"hi",47,11.1);
}
static FiveTuple<Vehicle,Amphibian,String,Integer,Double> h(){
return new FiveTuple<>(new Vehicle(),new Amphibian(),"hi",47,11.1);
}
public static void main(String[] args) {
TwoTuple<String, Integer> f = f();
System.out.println(f);
//f.a="there"; compile error :final
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
//运行结果为
TwoTuple{a=hi, b=47}
ThreeTuple{c=47, a=generic.Amphibian@ea2f77, b=hi}
FiveTuple{c=hi, d=47, e=11.1, a=generic.Vehicle@1c7353a, b=generic.Amphibian@1a9515}
FourTuple{c=47, d=11.1, a=generic.Amphibian@f49f1c, b=hi}
- 由于有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。而你所要做的,只是编写表达式而已。
- 通过 f.a ="there" 语句的错误,我们可以看出,final 声明确实能够保护 public 元素,在对象被构造出来之后,声明为 final 的元素便不能被赋予其他值了。
一个堆栈类
public class LinkedStack<T> {
private static class Node<U> {
U u;
Node<U> next;
public Node() {
u = null;
next = null;
}
public Node(U u, Node<U> next) {
this.u = u;
this.next = next;
}
boolean end() {
return u == null && next == null;
}
}
private Node<T> node = new Node<>();
void push(T t) {
node = new Node<>(t, node);
}
T pop() {
T result = node.u;
if (!node.end())
node = node.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> linkedList = new LinkedStack<>();
for (String s :"Phasers or stun!".split(" ")) {
linkedList.push(s);
}
String s;
while((s=linkedList.pop()) != null){
System.out.println(s);
}
}
}
//运行结果为
stun!
or
Phasers
- 内部类 Node 也是一个泛型,它拥有自己的类型参数。
- 这个 例子使用了一个 末端哨兵 来判断堆栈何时为空。这个 末端哨兵 是在构造 LinkedStack 时创建的。
- 然后每调用一次 push() 方法,就会创建一个Node<T> 对象,并将其链接到前一个 Node<T>对象。当你调用 pop() 方法时,总是返回 node.u ,然后丢弃当前 node 所指的 Node<T> , 并将 node 转移到下一个 Node<T> ,除非你已经碰到了 末端哨兵,这时候就不再移动 node 了。
- 如果已经到了末端,客户端程序还继续调用 pop() 方法,它只能得到 null, 说明堆栈已经空了。
RandomList
- 假设我们需要一个持有特定类型对象的列表,每次调用其上的 select() 方法时,它可以随机选取一个元素。
public class RandomList<T> {
private List<T> lists=new ArrayList<>();
private Random random=new Random(47);
public void add(T t){
lists.add(t);
}
T select(){
return lists.get(random.nextInt(lists.size()));
}
public static void main(String[] args) {
RandomList<String> randomList = new RandomList<>();
for (String s :"The quick brown fox jumped over ".split(" ")) {
randomList.add(s);
}
for (int i = 0; i < 5; i++) {
System.out.println(randomList.select());
}
}
}
//运行结果为
brown
over
quick
over
quick
泛型接口
- 泛型也可以应用于接口。例如生成器 ,这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。
public interface Generator<T> {
T next();
}
- 方法 next() 的返回类型是参数化的 T 。正如你所见的,接口使用泛型与类使用泛型没什么区别。
class Coffee {
static long counter = 0;
final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName()+" "+id;
}
}
class Latte extends Coffee {
}
class Mocha extends Coffee {
}
class Cappuccino extends Coffee {
}
class Americano extends Coffee {
}
class Breve extends Coffee {
}
- 现在我们可以编写一个类,实现 Generator<Coffee> 接口,它能随机生成不同类型的 Coffee对象。
class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
private Class[] classes = {
Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class
};
private Random random = new Random(47);
//无参构造器
public CoffeeGenerator() {
}
//for iteration 迭代
private int size = 0;
//有参构造器
public CoffeeGenerator(int size) {
this.size = size;
}
@Override
public Coffee next() {
try {
return (Coffee) classes[random.nextInt(classes.length)].newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count --;
return CoffeeGenerator.this.next();
}
//not implemented 未实现
@Override
public void remove(){
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
CoffeeGenerator coffees = new CoffeeGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(coffees.next());
}
for (Coffee coffee:new CoffeeGenerator(5)) {
System.out.println(coffee);
}
}
}
//运行结果为
Americano 0
Latte 1
Americano 2
Mocha 3
Mocha 4
Americano 5
Latte 6
Americano 7
Mocha 8
Mocha 9
- 参数化的 Generator 接口确保 next() 的返回值是参数的类型。
- CoffeeGenerator 同时还实现了 Iterator 接口,所以它可以在循环语句中使用。不过,它还需要一个 末端哨兵 来判断何时停止,这正是第二个构造器的功能。
public class Fibonacci implements Generator<Integer> {
private int count = 0;
@Override
public Integer next() {
return fib(count++);
}
private int fib(int n){
if (n<2) return 1;
return fib(n-1)+fib(n-1);
}
public static void main(String[] args) {
Fibonacci fibonacci = new Fibonacci();
for (int i = 0; i < 5; i++) {
System.out.println(fibonacci.next()+" ");
}
}
}
//运行结果为
1
1
2
4
8
- 虽然我们再 Fibonacci类里里歪歪使用的都是 int 类型,但是其类型参数确实 Integer。这个例子引出了Java 泛型的一个局限性: 基本类型无法作为类型参数。
- 不过,Java SE5 具备了自动打开和自动拆包的功能, 可以很方便地在基本和其相应的包装器类型之间进行转换。通过这个例子中 Fibonacci 类对 int 的使用,我们已经看到了这种效果。
- 如果还想进一步,编写一个实现了 Iterator 的Fibonacci 生成器。如下例子。
public class IteratorFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IteratorFibonacci(int n) {
this.n = n;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
@Override
public boolean hasNext() {
return n>0;
}
@Override
public Integer next() {
n--;
return IteratorFibonacci.this.next();
}
};
}
public static void main(String[] args) {
IteratorFibonacci fibonacci = new IteratorFibonacci(6);
for (int s : fibonacci) {
System.out.println(s+" ");
}
}
}
//运行结果为
1
1
2
4
8
16
- 如果要在循环语句中使用 IterableFibonacci ,必须向 IterableFibonacci的构造器提供一个边界值,然后 hasNext() 方法才能知道何时应该返回false。
泛型方法
- 到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
- 泛型方法使得该方法能够独立于类而产生变化。以下时一个基本的知道原则: 无论何时,只要你能做到,你就应该尽量使用泛型方法。
- 也就是说,如果使用泛型方法可以取代整个类泛型化,那么你就应该只使用泛型方法。因为它可以使事情更清楚明白。
- 另外,对于一个 static 的方法而言, 无法访问泛型类的类型参数,所以,如果static 需要使用泛型能力,就必须使其成为泛型方法。
public class GenericMethods {
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods methods = new GenericMethods();
//传入字符串
methods.f(" ");
//传入int类型
methods.f(2);
//传入double
methods.f(2.2d);
//传入float
methods.f(2.2f);
//传入对象
methods.f(methods);
}
}
//运行结果为
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
generic.GenericMethods
- GenericMethods 并不是参数化,尽管这个类和其内部的方法可以被同时参数化,但是在这个例子中,只有方法 f() 拥有类型参数。这是由该方法的返回类型前面的类型参数列表指明的。
- 注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这被称为 类型参数推断。
- 我们可以像调用普通方法一样调用 f() 方法,而且就好像是f() 方法被无限次地重载过。它甚至可以接受 GenericMethods 作为其类型参数。
- 如果调用 f() 时传入基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。事实上,泛型方法与自动打包避免了许多以前我们不得不自己编写出来的代码。(如 int的 值被打包机制转化为 Integer)。
杠杆利用类型参数推断
- 人们对泛型有一个抱怨,使用泛型有时候需要向程序中加入更多的代码。如下
Map<Person,List<? extends Pest>> petPeople=new HasMap<Person,List<? extends Person>>(;)
- 如上代码,我们总是在重复自己做过的事情,编译器本来应该能够从泛型参数列表中的一个参数推断出另一个参数。可惜的是,编译器暂时还做不到。然泛型方法中,类型参数推断可以为我们简化一部分工作。
- 例如,我们可以编写一个工具类,它包含各种各样的 static 方法,专门用来创建各种常用的容器对象。
public class New {
static <K, V> Map<K, V> map() {
return new HashMap<>();
}
static <T> List<T> list() {
return new ArrayList<>();
}
static <T> LinkedList<T> linkedList() {
return new LinkedList<>();
}
static <T> Set<T> set() {
return new HashSet<>();
}
static <T> Queue<T> queue() {
return new LinkedList<>();
}
public static void main(String[] args) {
Map<String, List<String>> map = New.map();
List<String> list = New.list();
Set<String> set = New.set();
LinkedList<String> linkedList = New.linkedList();
Queue<String> queue = New.queue();
}
}
- main() 方法演示了如何使用这个工具类,类型参数推断避免了重复的泛型参数列表。它同样可以应用于 如下
Map<Person,List<? extends Pet>> map=New.Map();
//...
- 如果某人阅读以上代码,他必须分析理解工具类 New ,以及New 所隐含的功能。而这似乎与不使用 New 时(具有重复的类型参数列表的定义)的工作效率差不多。
- 这真够讽刺的,要知道,我们引入New 工具类的目的,正是为了使代码简单易读。不过,如果 标准的Java类库要是能添加类似 New.java 这样的工具类的话,我们还是应该使用这样的工具类。
- Java 在判断类型的时候, 往往是在赋值操作的时候,能够通过参数化类判断出具体的类型。
public class LimitsOfInference {
public static <T> List<T> f() {
return new ArrayList<>();
}
public static void m(List<String> list){
System.out.println(list.getClass().getName());
}
public static void main(String[] args) {
List<String> objects = f();
m(objects);
}
}
//运行结果为
java.util.ArrayList
- 通过 List<String> 得知了返回的ArrayList<T> 中 T的类型为String ,因此 m 方法直接调用得到 运行结果。
- 如果 m() 方法直接调用 f() 方法,而不知道应该返回什么类型,默认是返回Object类型。
- 以ArrayList<String> 为参数,编译器不会自动转换,而是依然向里面传入 ArrayList<Object> 因此报错如下
//这里编译器永远不会把 Object 转化为 String
public class GenericDemo {
static ArrayList<String> f(){
return new ArrayList<>();
}
static void m(List<Object> list){
System.out.println(list.getClass().getName());
}
public static void main(String[] args) {
//通过泛型方法及类型推断的到 ArrayList<String>
ArrayList<String> strings = f();
// m(strings); m() 需要的是 List<Object> 集合而不是 List<String>集合
// m(f()); 同上
}
}
- 通过上面两段代码可知:
- 泛型方法在获取具体类型时,仅仅在赋值的时候有效,参数中不会自动转换。
//注意 不要将这两种情况搞混淆
//1 泛型赋值
//2 类继承使用
public class GenericDemo2 {
static String f() {
return new String();
}
static void m(Object object){
System.out.println(object.getClass().getName());
}
public static void main(String[] args) {
m(f());
}
}
//运行结果
java.lang.String
显式的类型说明
- 在泛型方法中,可以显式地指明类型,不过这种语法很少使用。要显式地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。
- 如果是在定义该方法的类的内部,必须在点操作符之前使用 this 关键字,如果是使用static 的方法,必须在点操作符之前加上类型。
public class ExplicitTypeSpecification {
static void f(Map<Person, List<? extends Person>> map) {
}
public static void main(String[] args) {
Map<Object, Object> map = New.map();
//f(map); 编译失败 无法把 Object 类型转化为 Person对象
f(New.<Person, List<? extends Person>>map());
//可以直接使用如下
f(New.map());
}
}
- 当然,这种语法抵消了 New 类为我们带来的好处(即省去了大量的类型说明),不过,只有在编写非赋值语句时,我们才需要这样的额外说明。
可变参数与泛型方法
- 泛型方法与可变参数列表能够很好地共存。
public class GenericVarargs {
static <T> List<T> makeList(T... args) {
ArrayList<T> list = new ArrayList<>();
Arrays.asList(args).forEach(each -> {
list.add(each);
});
return list;
}
public static void main(String[] args) {
List<String> list = makeList("A");
System.out.println(list);
list = makeList("A", "B", "C");
System.out.println(list);
list = makeList("QWERTYUIOPASDFGHJK".split(""));
System.out.println(list);
}
}
//运行结果
[A]
[A, B, C]
[Q, W, E, R, T, Y, U, I, O, P, A, S, D, F, G, H, J, K]
- makeList() 方法展示了 与 标准类库中 Arrays.asList() 方法
利用Generator的泛型方法
- 利用生成器,我们可以很方便地填充一个 Collection ,而泛型化这种操作是具有实际意义的。
public class Generators {
static <T> Collection<T> fill(Collection<T> collection,
Generator<T> generator,
int n) {
for (int i = 0; i < n; i++)
collection.add(generator.next());
return collection;
}
public static void main(String[] args) {
Collection<Coffee> coffees = fill(new ArrayList<Coffee>(), new CoffeeGenerator(), 4);
for (Coffee c : coffees) {
System.out.println(c);
}
Collection<Integer> collection=fill(new ArrayList<Integer>(), new Fibonacci(), 12);
for (int i : collection) {
System.out.println(i);
}
}
}
//运行结果为
Americano 0
Latte 1
Americano 2
Mocha 3
1
1
2
4
8
16
32
64
128
256
512
1024
- 请注意,fill() 方法是如何透明地应用于 Coffee 和 Integer 的容器和 生成器。
一个通用的 Generator
- 可以为任何类构造一个Generator , 只要该类具有默认的构造器。为了减少类型声明,它提供一个泛型方法,用于生成BasicGeneratror。
public class BasicGenericator<T> implements Generator<T> {
private Class<T> type;
public BasicGenericator(Class<T> type) {
this.type = type;
}
@Override
public T next() {
try {
return type.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
static <T> Generator<T> create(Class<T> type) {
return new BasicGenericator<T>(type);
}
}
- 这个类提供了一个基本实现,用于生成某个类的对象。这个类具备两个特点
- 它必须声明为 public (因为 BasicGenerator 与要处理的类在不同的包中,所以该类必须声明为 public ,并且不只具有包内访问权限)。
- 它必须具备默认的构造器,(无参数的构造器)。
public class CountedObject {
private static long counter = 0;
private final long id =counter++;
public long getId(){
return id;
}
@Override
public String toString() {
return "CountedObject"+id;
}
}
class BasicGeneratorDemo{
public static void main(String[] args) {
Generator<CountedObject> generator= BasicGenericator.create(CountedObject.class);
for (int i = 0; i < 5; i++) {
System.out.println(generator.next());
}
}
}
//运行结果
CountedObject0
CountedObject1
CountedObject2
CountedObject3
CountedObject4
- 可以看到 ,使用泛型方法创建Generator对象,大大减少了我们要编写的代码。
- Java泛型要求传入Class对象,以便也可以在 create()中用它进行类型推断。