一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的,如果要编写可以应用与多种类型的代码,就要使用到泛型。泛型实现了参数化类型的概念, 使代码可以应用于多种类型。
1、简单泛型
package com.example.cleancode.chapter9;
public class Holder<T> {
private T a;
public Holder(T a) {
this.a = a;
}
public void set(T a) {
this.a = a;
}
public T get() {
return a;
}
public static void main(String[] args) {
Holder<Automobile> h =
new Holder(new Automobile());
Automobile a = h.get(); // No cast needed
//h.set("Not a Automobile");//Error
//h.set(1);//Error
h.set(new MyAutomobile());
}
}
class Automobile {
}
class MyAutomobile extends Automobile {
}
在Hoder类定义中,将类型参数使用尖括号括住,放在类名后面,然后在使用这个类型的时候,在用实际的类型替换此类型参数,其中T就是类型参数,在main() 方法中使用Automobile 代替T的位置,完成对象初始化,然后只能在Holder中存入Automobile 及其子类型的对象。
元组类库:
使用元组(tuple),可以调用一次方法就能返回多个对象,它是将一组对象直接打包储存在其中一个单一对象。这个容器对象允许读取其中的元素,但是不允许向其中存放新的对象。
通常,元组是可以具有任意长度,同时,元组中的对象也可以是任意不同的类型,下面的程序是一个二维元组,它能够持有两个对象:
public class TwoTuple<A, B> {
public final A first;
public final B second;
public TwoTuple(A a, B b) {
first = a;
second = b;
}
@Override
public String toString() {
return "TwoTuple{" +
"first=" + first +
", second=" + second +
'}';
}
}
以下是测试类:
import org.junit.Test;
public class TupleTest {
static TwoTuple<String, Integer> f() {
return new TwoTuple("hi", 47);
}
@Test
public void test() {
System.out.println(f());
}
}
此时你可能会想,这不是违反了Java编程的安全性原则吗? first和second应该声明为private,然后提供getFirst()和getSecond()之类的访问方法才对呀?让我们仔细看看这个例子中的安全性:客户端程序可以读取firs在和second对象,然后可以随心所欲地使用这两个对象。但是,它们却无法将其他值赋予first或second。因为final声明为你买了相同的安全保险,而且这种格式更简洁明了。
堆栈类:
通过代替LinkedList ,来实现自己的内部链式储存机制。以下是对象的定义:
/**
* 堆栈类
* @param <T>
*/
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
Node() {
item = null;
next = null;
}
Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() {
return item == null && next == null;
}
}
private Node<T> top = new Node(); // End sentinel
public void push(T item) {
top = new Node(item, top);
}
public T top() {
T result = top.item;
if (!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> linkedStack = new LinkedStack<>()
for (String s : "Phasers or stun!".split(" ")) {
linkedStack.push(s);
}
String s;
while ((s = linkedStack.top()) != null) {
System.out.println(s);
}
}
}
其中内部类Node是一个泛型,它拥有自己的类型参数。
这个例子中使用了末端哨兵(end sentinal)来判断堆栈何时为空。这个末端哨兵是在构造LinkedStack时创建的,在进行出栈操作时,当碰到末端哨兵时候,这个时候就不在移动top了,代表已经到了末端,这个时候top() 方法返回为null, 说明堆栈已经为空了。
RandomList:
作为容器的一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其中的select()方法时候,它可以随机选取一个元素。如果我们希望以此构建一个可以应用于各个类型的对象的工具,就需要是泛型。
import java.util.ArrayList;
import java.util.Random;
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList();
private Random random = new Random(47);
public void add(T item) {
storage.add(item);
}
public T select() {
return storage.get(random.nextInt(storage.size()));
}
public int size() {
return storage.size();
}
public static void main(String[] args) {
RandomList randomList = new RandomList();
for (String s : ("the quick brown for jumped over " + "the lazy brown dog").split(" ")) {
randomList.add(s);
}
for (int i = 0; i < randomList.size(); i++) {
System.out.print(randomList.select() + " ");
}
}
}
2、泛型接口
泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。
import java.util.Iterator;
import java.util.Random;
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = {Latte.class, Mocha.class, Cappuccino.class,
Americano.class, Breve.class};
private Random random = new Random();
public CoffeeGenerator() {
}
public CoffeeGenerator(int size) {
this.size = size;
}
private int size = 0;
@Override
public Coffee next() {
try {
return (Coffee) types[random.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count--;
return new CoffeeGenerator().next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee c : new CoffeeGenerator(5)) {
System.out.println(c);
}
}
}
参数化的 Generator 接口确保 next() 的返回值是参数的类型。CoffeeGenerator 同时还实现了 Iterable 接口,所以它可以在循环语句中使用。不过它需要一个"末端哨兵"来判断何时停止,这正是第二个构造器的功能。
下面的类是Generator<T>接口的另一个实现,它负责生成Fibonacci数列:
public class Fibonacci implements Generator<Integer> {
private int count = 0;
public Integer fib(int count) {
if (count < 2) return 1;
return fib(count - 1) + fib(count - 2);
}
@Override
public Integer next() {
return fib(count++);
}
public static void main(String[] args) {
Fibonacci fibonacci = new Fibonacci();
for (int i = 0; i < 18; i++) {
System.out.print(fibonacci.next() + " ");
}
}
}
如果还想更进一步,编写一个实现了Iterable的Fibonacci 生成器,我们的选择是重写这个类,令其实现Iterable接口,而且我们还有另一个选择,就是创建一个适配器(adapter)来实现所需的接口,下面通过继承来创建适配器类:
import java.util.Iterator;
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(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 IterableFibonacci.super.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(int i: new IterableFibonacci(18)){
System.out.print(i+" ");
}
}
}
3、泛型方法
以上使用的泛型,都是应用于整个类上,但是同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public <K,V> void m(K k,V v){
System.out.println(k.getClass().getName());
System.out.println(v.getClass().getName());
}
public <M> void s(int i,M m){
System.out.println(m.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("hello");
gm.f(1);
gm.f(1.0);
gm.f(2.0D);
gm.f('c');
gm.f(gm);
}
}
杠杆利用类型参数判断:
编译器本来应该能够从泛型参数列表中的一个参数推断出另一个参数。可惜的是,编译器暂时做不到。然后,在泛型方法中,类型参数判断可以为我们简化一部分工作。
4、边界
为了能够将这个参数限制在某个类型子集,Java泛型重用了extends关键字。
import java.awt.*;
interface HasColor {
java.awt.Color getColor();
}
class Colored<T extends HasColor> {
T item;
Colored(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
}
class Dismension {
public int x, y, z;
}
class ColorDimension<T extends Dismension & HasColor> {
T item;
ColorDimension(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
interface Weight {
int weight();
}
class Solid<T extends Dismension & HasColor & Weight> {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded extends Dismension implements HasColor, Weight {
@Override
public Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
System.out.println(solid.color() + "-" +
solid.getY() + "-" +
solid.weight());
}
}
5、通配符
通配符只有在修饰一个变量时会用到,使用它可方便地引用包含了多种类型的泛型;
public class CovariantArrays {
public static void main(String[] args) {
//不使用通配符
ArrayList<Object> arr = new ArrayList<Object>();
// ArrayList<Object> arr = new ArrayList<String>(); 编译不通过,arr只能引用包含Object的集合
//使用通配符
ArrayList<?> arr2;
arr2 = new ArrayList<String>();
arr2 = new ArrayList<Integer>();
arr2.get(0); //返回的,是一个Object对象,通配符会使原集合包含类型信息丢失,也是通配符的使用代价
// 通常在方法参数中才会使用通配符,使得这个方法可以引用多种泛型集合。这个和范型方法不一样,这里只是一个引用变量
}
void gMethod(ArrayList<? extends Number> param) {
}
}
可以看到,通配符使用方便的同时,使原集合包含类型信息丢失。
通配符的extends super关键字
ArrayList<? extends Number> arr3; // Number 是 Integer、Float的父类; ArrayList<Number> arr3只能引用 ArrayList<Number>
arr3 = new ArrayList<Integer>();
arr3 = new ArrayList<Float>();
// arr3 = new ArrayList<String>(); 编译不通过,String 和 Number不存在继承关系
arr3.get(0); //返回的,是一个Number对象
arr3.add(null); //使用过通配符修饰的集合变量,只能add(null),因为这个集合中包含的类型已经确定,只是类型信息已经丢失了,add(Object)也不行
无限定通配符
ArrayList<?> arr3; 无通配符等同于 ArrayList<? extends Object> arr3; //用于取值get(),不能赋值set()
不使用泛型的变量和另一种方式
//这样使用功能和通配符一样,可以多种引用,但一般不推荐这样使用
ArrayList a4;
a4 = new ArrayList<String>();
a4 = new ArrayList<Integer>();
a4.add(new Integer(1));
a4.add(new String("str"));//和通配符引用不能add不一样,这样方式可以add多种类型元素,但一般不推荐
a4.get(0); //返回的,是一个Object对象
固定下边界通配符的使用
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
addNumbers(list1);
System.out.println(list1);
List<Number> list2 = new ArrayList<>();
addNumbers(list2);
System.out.println(list2);
List<Double> list3 = new ArrayList<>();
// addNumbers(list3); // 编译报错
}
我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代码:
public static void getTest2(List<? super Integer> list) {
// Integer i = list.get(0); //编译报错
Object o = list.get(1);
}
这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型。