用几个简单例子理解Java的泛型并用好他

前几天和小伙伴在食堂吃饭时,正好说到了泛型,再把泛型的几个要素总结下,下面用几个简单的例子说明下,如何用好泛型。

泛型的定义

这个大家应该都不陌生,泛型是在Java1.5之后引入的,字面定义是: 声明中,具有一个或者多个类型参数的类或者接口,就是泛型类或者接口。构成格式是,类或者接口,接着用<>尖括号把对应于泛型形式类型参数的实际类型参数列表括起来。例如List\<E>就是泛型,List就是原生态类型,而每一个泛型都对应着这么一个原生态类型。

一、 能用泛型,就不要用原生类型,比如下面这个例子
public void testGeneric() {
List origin = new ArrayList();
origin.add(12L);
origin.add("abcdefg");
for(Object t : origin) {
long p = (long)t;
}
}

 

上面那段代码,在编译的时候,不会有任何错误(会有一个警告), 然而在运行的时候,才报出错误

我们的代码当然是越早发现错误越好,而泛型则会在你定义之后,在编译阶段帮你校验你放进去的到底是不是他所需要的类型。

可能你会觉得,我不会犯这样的错误,我怎么会把string放进去呢,但是如果这个结果来自于别人给你提供的服务呢,类似下面代码

import java.sql.Date;
//....这里省略一些代码
public void testGenericB() {
List origin = new ArrayList();
origin.add(new Date(124L));
origin.addAll(DateClassB.getDate());
for(Iterator i = origin.iterator(); i.hasNext();) {
Date p = (Date)i.next();
}
}

//这个帮助类实际上返回的是util.Date
import java.util.Date;
//...这里省略一些代码
public static List getDate() {
List date = new ArrayList();
date.add(new Date(123));
return date;
}

 

DateClassB.getDate给你返回的是Date对象,就坦然的放进了你的List中,等到另外一个地方,你要用的时候,出现了转换错误。

当然,如果你注意到了你的IDE给你的警告提示,也许你可以避免这个问题。但是需要说的是,能用泛型的地方不要使用原生类型,他们只是为了兼容Java1.5以前的版本,其他没什么好处。

二、 List 和 List\<Object\> 的区别

他们都可以往里面放任何的对象,那么他们的区别是什么呢,看看下面这段代码

public void testGenericC() {
List<Long> longData = new ArrayList<Long>();
addData(longData, "abcdefg");

}

private void addData(List list, Object o) {
list.add(o);
//更多其他逻辑省略
}

 

在addData这个方法中,传入的是List的原生态类型,导致了运行时出现了ClassCastException,而如果写成下面这个呢?

private void addData(List<Object> list, Object o) {
list.add(o);
//更多其他逻辑省略
}

 

 

这个就会在你的编译期间直接报错,因为List\<String\> is not List\<Object\>,但是List\<String\>却是List。这个就是他们之间的区别。

 三、 List和List\<?>以及List\<Object>的区别: 无限通配符

我们都见过也知道List<? extends SuperClass>这种限制类型的通配符, 表示的是声明这个list里装的都是Super类型的对象,还可以放入他的子类。但是?则代表的是无线通配符,他是实参可不是形参。

在上面的例子中看到,addData方法使用了List\<Object\>导致了错误, 那么如果遇到了确实想传入各种类型的List到addData方法中,该怎么办呢,看看如下的代码就明白了

 

public void testprintData() {
List<Long> longData = new ArrayList<>();
longData.add(12345L);
List<String> stringData = new ArrayList<>();
stringData.add("abcdefg");
printData(longData);
printData(stringData);

}

//使用无限通配符可以轻松解决这个问题
private void printData(List<?> list) {
System.out.print("data is " + list.get(0));
}

 

 

在最小范围内使用SuppressWarnings("unchecked")

在使用泛型的时候,总是会遇到很多的unchecked cast warnings和unchecked conversion warning,这个时候你就可以用@SuppressWarnings("unchecked")来消除这些警告,前提是你要能够确定自己的类型一定是安全的。也就是说要谨慎的放在整个类上面,他会让你错过很多有用信息。

这个注解即可以用到类上面,还可以用到方法上面,如下图


但是我们要遵循一个原则,就是在最小范围内放在我们确定的代码上生效,不要放过那些自己没有check过的代码上.

@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
Object[] origin = new Object[10];
if (a.length<10) {
return (T[])Arrays.copyOf(origin,10,a.getClass());
}
return a;

}

 

由于你没办法把SuppressWarning放到return语句上, 你不得不放到了方法上面,这样会导致方法内所有的警告都没有了,无论是你有意还是无心.

//最小范围内使用
public <T> T[] toArray(T[] a) {
Object[] origin = new Object[10];
if (a.length<10) {
@SuppressWarnings("unchecked")
T[] p = (T[])Arrays.copyOf(origin,10,a.getClass());
return p;
}
return a;

}

我们可以按照第二种方法,用一个临时变量来换下return,这样就能把SuppressWarning用在这一句话上了。

五、数组和List的区别

数组是协变的,而泛型是类型擦除的。数组在运行的时候去校验类型的正确性,而泛型是在编译的时候去校验,在运行的时候不知道他是什么类型的。

协变的意思是,String[]是Object[]的子类,所有要求传入Object[]的地方, 都可以传入String[]来代替。

public void testArray() {
Object[] p = new Long[8];
p[0] = "String";
p[1] = 123;
p[2] = new java.util.Date();

}

以上代码编译没错,但是运行时会产生一个ArrayStoreException

而List\<Long>和List\<Object>却一点关系都没有,当然和List\<? extends Object>还是有关系的,那是另一个层面

List<Object> object = new ArrayList<Long>();//这里就不通过
ojbect.add("abcd");

以上代码则编译的时候就出错了.

类型擦除的意思是,在运行的时候,那个类型参数E是啥,他根本不知道,所以他也就没有任何办法进行
E[] = new E[]; 因为他不知道E是啥,所以他无法去new他。

 

六、 泛型的公式, PECS。在设计API的时候尽量使用有限通配符。

在我们提供给别人使用的API的时候,我们使用了泛型,虽然很灵活,但是一旦这个类被构建出来,代表泛型的那个参数就固定下来了,想要再灵活一点就做不到了,例如下面的代码

 

public class DataManager<E> {
private List<E> list = new ArrayList<>();
public DataManager() {

}
public E getData() {
return list.get(list.size()-1);
}

public void putAll(List<E> src) {
list.addAll(src);
}
}

public class Rectangle {
}
public class Square extends Rectangle {
}

public void test() {
DataManager<Rectangle> a = new DataManager<>();
List<Square> squares = new ArrayList<>();
squares.add(new Square());
//尽管squares也是Rectangle,但是我这里放不进去
a.putAll(squares);
}

 

 

在上面代码中, 尽管Square是Rectangle子类,但是在putAll的时候,你却不能把List<Square>传进去,尽管Square就是Rectangle的子类,但是List\<Square> 和 List\<Rectangle>却是两码事。
所以这个时候把函数的定义改成这样的, 就解决了问题,让你的API就更加灵活了。

 

public void putAll(Collection<? extend E> src) {
list.addAll(src);
}

 

 

同理,我们增加这么一个方法

// 将里面的元素用out拷贝出来
public void getAll(List<E> out) {
out.addAll(list);
}

public void test2() {
DataManager<Square> a = new DataManager<>();
List<Square> in = new ArrayList<>();
in.add(new Square());
in.add(new Square());
a.putAll(in);

//我想用一个父类用来转移a里面的内容
List<Rectangle> out = new ArrayList<>();
//里面明明是Rectangle类型的,却不让我转移
a.getAll(out);
}

我想把里面的元素装进他的父类,结果不行,这不合乎情理也不易扩展,
稍加改造

public void getAll(List<? super E> out) {
out.addAll(list);
}

 

所以这里引出我们的结论,PECS公式,他的含义是producer-extends,consumer-super.

在putAll中,我们的对象是一个生产者把对象加到自己的里面来,在里面产生了一个个对象,所以使用extends.而在getAll中,我们的对象则被out给消费掉了,所以使用了super关键字,虽然对API提供者来说,增加了复杂性,但是对于使用者来说,他并不需要关心这些细节,更加灵活。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注