前几天和小伙伴在食堂吃饭时,正好说到了泛型,再把泛型的几个要素总结下,下面用几个简单的例子说明下,如何用好泛型。
泛型的定义
这个大家应该都不陌生,泛型是在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提供者来说,增加了复杂性,但是对于使用者来说,他并不需要关心这些细节,更加灵活。