由于本人能力有限,一下为个人理解的观点,如果错误,请留言,我会在验证后修改。
为什么使用泛型?
泛型简而言之就是当我们定义类,接口,和方法的时候允许类型参数化,通过这种方式我们可以实现代码的复用。
使用泛型有几点好处:
1)在编译时更强大的类型检查。
2)消除强制类型转化
3)通过使用泛型可以实现通用算法(比如集合框架)
泛型类和泛型方法
class A <T> {
private T i;
public T getI() {
return i;
}
public void setI(T i) {
this.i = i;
}
public <U> void method(U s,T a) {
}
}
原始类型
当我们定义了一个泛型类,在不指定泛型参数的情况下创建对象,,那么这就是原始类型的对象。需要说明一点的时无泛型的类和接口不是原始类型。
例如:
@Test
public void dome() {
//泛型
NOGBox<Integer> intBox = new NOGBox<>();
//原始类型
NOGBox box = new NOGBox();
}
因为在JDK5.0之前有很多的原始类型,为了向后兼容我们可是用原始类型去接受泛型类型的对象:
//泛型
NOGBox<Integer> intBox = new NOGBox<>();
//原始类型
NOGBox box = intBox;
但是如果你使用泛型类型去接受一个原始类型的对象会出现warning:
@Test
public void dome() {
//泛型
Box<Integer> intBox ;
//原始类型
Box box = new Box();
intBox = box;
System.out.println(box);
}
当我们讲到类型擦除部分有更多关于Java编译器如何使用原始类型的信息。
有界类型参数
有时候我们可能会想在泛型中限制泛型类型,例如一个方法操作一个变量可能想接收Number或者他的子类的对象,这就是限制泛型类型的作用。
关于限制的声明我们这样来做:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
//此处加了泛型类型的限制
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
还需要提一点的是但我们进行了泛型边界的限定之后,我们就可以调用调用定义在边界的对象的方法。可能这么说不容易理解我们可以参看代码:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
//此处调用了泛型边界Integer的
//intValue()方法
return n.intValue() % 2 == 0;
}
// ...
}
多重边界
上面讲解了单层边界,下面我们讲解多重边界。
//多重边界
<T extends B1 & B2 & B3>
需要注意的时,如果其中有一个类的话,他一定是在第一个位置,如果没有这麽做的话,将会报编译时异常。
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
边界类型参数是一个非常重要的概念,他是实现通用算法的关键,假设我们有一个数组,我们计算数组中比给定参数大的个数
我们是不是会这麽写:
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
但是返现报错了,这是应为只有基本数据类型可以使用>号进行判断。为了解决这个问题我们可以使用边界类型了:
/*
* public interface Comparable<T> {
public int compareTo(T o);
}
*/
//此处加了泛型边界,上面是Comparable接口的描述,这样我们就可以实现++了
public<T extends Comparable<T>> int count(T[] array,T element) {
int counts = 0;
for (int i = 0; i < array.length; i++) {
if(array[i].compareTo(element) > 0) {
counts++;
}
}
return counts;
}
泛型下继承关系
只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象。例如,您可以将一个Integer分配给一个Object,因为Object是Integer的超类型之一,泛型也是如此。您可以执行泛型类型调用,将Number作为其类型参数传递,如果参数与Number兼容,则允许任何后续的add调用
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
关于泛型的继承关系,这是我们经常在程序中犯的错误:
我们那 集合框架来看一看:
但我们定义一个自己的List<E>的实现类:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
并且声明了一下几个对象:
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>
我们在来看看他们的继承关系:
这个概念不好解释,我感觉还是看自己的理解比较重要就不过多的解释了
通配符
通配符有三种使用方式:
限定上边界
无边界
限定下边界
one by one
限定上边界(?extends xxx)
例如如果你想接一个方法使其能够工作于List<Integer>, List<Double>, and List<Number>我们可以这样写:
public static void process(List<? extends Number> list) { /* ... */ }
在函数里面我们就可以使用Number中定义个任何方法:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
//Number的成员函数
s += n.doubleValue();
return s;
}
无边界(?)
我们大部分使用无边界的场景如下:
1)如果您正在编写的方法全部使用Object类中提供的功能实现。
2)当代码使用泛型类中不依赖于类型参数的方法时。例如:
public static void printList(List<?> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
无边界其实也是比较常用的,比如我们的Class类就是,这是因为他的所有方法都不依赖与参数类型。
还有一个观点我们需要明白的就是List<Objecg> 和List<?> 是不同的,这是因为在List<Objecg>中你可以插入Object类型和其子类型的对象,但是你不能插入null.
限定下边界<? super XXX>
假设您想编写一个将Integer对象放入列表的方法。为了最大限度地提高灵活性,您希望该方法在List <Integer>,List <Number>和List <Object>上工作 -- 任何可以保存Integer值的东西。这是我们的<? super Integer>就闪亮出场了~!
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
通配符和其继承关系
尽管Number是Integer的父类,但是List<Number> 和 List<Integer> 就是没有任何关系。
为了在代码中通过Number的方法访问List<Integer>的元素,我们可以创建一个关系(通过使用限制上边界的方式):
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a //subtype of List<? extends Number>
这可能有些难以理解我们来解释一下:因为Integer是Number的子类,并且numList是一个存储Number元素的List对象,那么这个关系就是成立的,如果还不能理解的话我们可以查看下面的图解:
范围限定的使用指南
说到这里可能我们会为如何使用限制上边界 无边界 限制下边界很是苦恼,下面我们就来谈谈他们的使用场景。
为了方便讨论我们给出两个思想
In
“in”:将数据提供给代码去执行某些功能。想象一下带有两个参数的复制方法:copy(src,dest)。 src参数提供要复制的数据,因此它是“in”。
Out
不用说dest就是out了
好了下面我们就来说一说他们各自的应用场景
“in“变量用限制上边界的通配符来定义,使用extends关键字。
"out"变量使用限制下边界通配符来定义,使用super关键字。
方法中使用的都是Object类中定义的方法去访问“in”变量的情况下,使用无界通配符
在方法中需要以“in”和“out”变量自身的方法去访问变量的情况下,不要使用通配符。
以我们的编程功底可能写不出非常好的泛型类,但是通过对泛型的认知,我们可以更好的理解现有的框架比如Collection