您的位置:首页 > Web前端

JAVA 泛型总结(结合JAVA核心技术和Effective Java两书)

2016-12-18 15:23 405 查看

一、基础知识

1、类型擦除

类型擦除指的是通过类型参数合并, 将泛型类型实例关联到同一份字节码上。 编译器只为泛型类型生成一份字节码, 并将其实例关联到这份字节码上, 因此泛型类型中的静态变量是所有实例共享的。

(1) 一个static 方法, 无法访问泛型类的类型参数, 因为类还没有实例化, 所以, 若static方法需要使用泛型能力, 必须使其成为泛型方法,(泛型参数稍后会介绍),即没有成为泛型方法的静态方法不能访问类型参数。

(2) 泛型类的静态上下文中类型变量无效,即静态变量不能引用类型变量。

public class Singleton<T>{
private static T singleInstance;//错误,违反第二条
public static T getSingleInstance() // 错误,违反第一条
{
if(singleInstance == null)
Return singleInstance;
}
}

(3) 在使用泛型时, 任何具体的类型都被擦除, 唯一知道的是你在使用一个对象。 比如:List<String>和List<Integer>在运行事实上是相同的类型。 他们都被擦除成他们的原生类型,即List。 因为编译的时候会有类型擦除, 所以不能通过同一个泛型类的实例来区分方法, 如下面的例子编译时会出错, 因为类型擦除后, 两个方法都是List 类型的参数,
因此并不能

根据泛型类的类型来区分方法。

/*会导致编译时错误*/
public class Erasure{
public void test(List<String> ls){
System.out.println("Sting");
}
public void test(List<Integer> li){
System.out.println("Integer");
}
}
(4) 所有泛型类的实例都共享同一个运行时类,
类型参数信息会在编译时被擦除。 因此考

虑如下代码, 虽然 ArrayList<String>和 ArrayList<Integer>类型参数不同, 但是他们都共享

ArrayList 类, 所以结果会是 true。
List<String>l1 = new ArrayList<String>();
List<Integer>l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //True
(5) 不能对确切的泛型类型使用instanceOf 操作。因为instanceof会在运行时检测对象的类型,而泛型在运行时已经被擦除了,所以所有的List都是一样的,set也都是一样的。如下面的操作是非法的, 编译时会出错。

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>则不会出错。
(6)
不能用基本类型实例化类型参数, 因此吗, 没有 Pair<double>,只有Pair<Double>,原
因是类型擦除, 擦除后, Pair类含有Object类型的域, 而Object不能存储doble
值。

2、泛型和子类型

List<Apple> apples = new ArrayList<Apple>(); //right
List<Fruit> fruits = apples; //error
我们假定第2行代码没有问题,
那么我们可以使用语句fruits.add(new Strawberry())

Strawberry
为 Fruit的子类) 在fruits中加入草莓了,
但是这样的话, 一个List中装入了
各种不同类型的子类水果, 这显然是不可以的, 因为我们在取出 List中的水果对象时, 就
分不清楚到底该转型为苹果还是草莓了。
通常来说,如果 Foo是Bar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>
的子类型。

3、通配符

(1)<?>非限定通配符

//使用通配符? , 表示可以接收任何元素类型的集合作为参数
public void printCollection(Collection<?> c) {
for (Object e:c) {
System.out.println(e);
}
}
这里使用了通配符? 指定可以使用任何类型的集合作为参数。 读取的元素使用了 Object 类型来表示, 这是安全的, 因为所有的类都是 Object 的子类。 这里就又出现了另外一个问题,如下代码所示, 如果试图往使用通配符? 的集合中加入对象, 就会在编译时出现错误。 需要注意的是, 这里不管加入什么类型的对象都会出错。这是因为通配符?
表示该集合存储的元素类型未知, 可以是任何类型。 往集合中加入元素需要是一个未知元素类型的子类型, 正因为该集合存储的元素类型未知, 所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型, 所以尽管元素类型不知道, 但是null一定是它的子类型。
Collection<?> c=new ArrayList<String>();
c.add(newObject()); //compile time error, 不管加入什么对象都出错, 除了 null 外。
c.add(null); //OK
(2)限制性通配符:
<? extends A>从一个数据类型里获取数据
<? super B>把对象写入一个数据结构里
假定有一个画图的应用, 可以画各种形状的图形, 如矩形和圆形等。 为了在程序里面表示,
定义如下的类层次:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x,y,radius;
public void draw(Canvas c) { ... }
}
public class Rectangle extends Shape
private int x,y,width,height;
public void draw(Canvas c) { ... }
}
如果我们希望在List<?exends Shape> shapes 中加入一个矩形对象, 如下所示:
shapes.add(0, new Rectangle()); //compile-time
error

那么这时会出现一个编译时错误,
原因在于: 我们只知道 shapes
中的元素时 Shape类型的子类型, 具体是什么子类型我们并不清楚, 所以我们不能往shapes中加入任何类型的对象。不过我们在取出其中对象时,
可以使用 Shape
类型来取值, 因为虽然我们不知道列表中的元素类型具体是什么类型, 但是我们肯定的是它一定是Shape类的子类型。 所以可以用Shape接收取出的对象,
但不能向其中添加对象。

List<Shape> shapes = new ArrayList<Shape>();
List<? super Cicle> cicleSupers = shapes;
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK
cicleSupers.add(new Shape()); //ERROR
这表示cicleSupers 列表存储的元素为 Circle
的超类, 因此我们可以往其中加入 Circle对象或者Circle 的子类对象, 但是不能加入 Shape 对象。 这里的原因在于列表 cicleSupers存储的元素类型为Cicle 的超类, 但是具体是 Cicle 的什么超类并不清楚。 但是我们可以确定的是只要是 Cicle 或者 Circle 的子类, 则一定是与该元素类别兼容。

4、泛型方法

泛型方法的格式, 类型参数<T>需要放在函数返回值之前。 然后在参数和返回值中就可以使用泛型参数了。

public static <T> void fromArrayToCollection(T[] a, Collection<T>c){
for(T o : a) {
c.add(o);// correct
}
}
调用方法如下:
Object[] oa = new Object[100];
Collection<Object>co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T inferred to be Object
我们调用方法时并不需要传递类型参数,系统会自动判断类型参数并调用合适的方法。 当然

在某些情况下需要指定传递类型参数, 比如当存在与泛型方法相同的方法的时候( 方法参数

类型不一致) , 如下面的一个例子:
public <T> void go(T t) {
System.out.println("generic function");
}
public void go(String str) {
System.out.println("normal function");
}
public static void main(String[] args) {
FuncGenric fg = new FuncGenric();
fg.go("haha");//打印 normal function
fg.<String>go("haha");//打印 generic function
fg.go(new Object());//打印 generic function
fg.<Object>go(new Object());//打印 generic function
}

5、方法重载

在 JAVA 里面方法重载是不能通过返回值类型来区分的, 比如代码一中一个类中定义两个如

下的方法是不容许的。 但是当参数为泛型类型时, 却是可以的。 如下面代码所示, 虽然形参

经过类型擦除后都为 List 类型, 但是返回类型不同, 这是可以的。

/*代码: 正确 */
public class Erasure{
public void test(List<String> ls){
System.out.println("Sting");
}
public int test(List<Integer> li){
System.out.println("Integer");
}
}

6、泛型数组

不能创建一个确切泛型类型的数组。 如下面代码会出错。

List<String>[] lsa = new ArrayList<String>[10]; //compile error.
因为如果可以这样, 那么考虑如下代码, 会导致运行时错误。

List<String>[] lsa = new ArrayList<String>[10]; // 实际上并不允许这样创建数组
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer>li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;// unsound, but passes run time store check
String s = lsa[1].get(0); //run-time error - ClassCastException
因此只能创建带通配符的泛型数组, 如下面例子所示, 这回可以通过编译, 但是在倒数第二行代码中必须显式的转型才行, 即便如此, 最后还是会抛出类型转换异常,
因为存储在 lsa中的是 List<Integer>类型的对象, 而不是 List<String>类型。 最后一行代码是正确的, 类型匹配, 不会抛出异常。

List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer>li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; //correct
String s = (String) lsa[1].get(0);// run time error, but cast is explicit
Integer it = (Integer)lsa[1].get(0); // OK

7、不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象, 甚至泛型类扩展
Throwable 都是不合法的。如, 以下定义不能正常编译:
public class Problem<T> extends Exception{}//Error
catch
子句中不能使用类型变量, 以下方法不能编译:
public static <T extends Throwable> void dowork(class<T> t)
{
 try {}
 catch(T e) //error
 {}
}
不过,
在异常规范中使用类型变量是允许的, 以下方法合法:
Public static <T extends Throwable> void doWork(T t) throws T

 try{}
 Catch(Throwable realCause)
 {
   t.initCause(realCause);
   throw t;
  } 
}

二、使用规范

第23条:不要在新代码中使用原生态类型

在1.5版本以后的代码建议使用泛型而不是对应的原生态类型,这有助于错误的发现,使用Java的特性,避免强制转换。是用原生态类型失去了泛型在安全性和表述性方面的所有优势。

如果一些情况下客户代码不在乎类型参数T到底是什么的话,也建议使用泛型,如<?>,表示接受任何类型,相比于“T”限制了某一类型,“?”的范围过得多(?被成为无限制的通配符)。

总之,使用原生态类型会在运行时导致异常,因此不要在新代码中使用。Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合;Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合,Set则是原生态类型,他脱离了泛型系统。前两种是安全的,最后一中不安全。

第24条:消除非受检警告

在使用泛型时,会遇到许多编译器警告:非受检强制转化警告、非受检方法调用警告、非受检普通数组创建警告、以及非受检转换警告。 
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下)可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告。
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解。它通常是个变量声明,或是非常简短的方法或者构造器。永远不要再整个类上使用SuppressWarnings,那么做可能会掩盖了重要的警告。

第25条:列表优先于数组

数组和泛型的区别:
(1)数组是协变的,即如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的,即对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。
(2)数组是具体化的,因此数组会在运行时才知道并检查他们的元素类型约束。而泛型则是通过擦除实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或擦除)它们的元素类型信息。
由于数组和泛型之间的根本区别,数组和泛型不能很好的混合使用。创建泛型、参数化类型或者类型参数的数组都是非法的。如果混合使用出现了编译时错误或者警告,那么应该用列表代替数组

第26条:优先考虑泛型

因为声明一个泛型数组是合法的,而new一个泛型数组是不合法的,那么当自定义一个泛型时,内部需要使用到泛型数组,如何new呢?
一种方法是先new 一个Object数组,然后强制转换成对应的泛型的,编译器会警告这不是类型安全的,但我们在检查后确认无误,可以这样使用。
例如:E[] elements = (E[])new Object[100];

第二种方法是:将E[]改为Object[],即Object[]
elements = new Object[100];但是在获取对象后,要将对象强转为E即E result = (E)elements[0];这样做编译器会警告这是不安全的,可以通过@SuppressWarnings("unchecked")来解除警告。

第27条:优先考虑泛型方法

使用泛型方法可以不用在代码中显示进行类型转换,还可以进行类型推导。

(1).编写一个集合并集的泛型方法:
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}


(2).泛型类型推导:

正常声明泛型集合如下:

Map<String, List<String>>anagrams = new HashMap<String, List<String>>();

通过集合声明时的泛型参数类型就可以推导出集合实例的泛型参数类型,这个过程叫泛型类型推导,如果支持泛型类型推导,则上面代码的HashMap就可以不再指定泛型参数类型,但是目前JDK还没有内置泛型类型推导,我们可以自己进行一个小的模拟实现:
public static <K, V> HashMap<K, V> newHashMap(){
return new HashMap<K, V>();
}
Map<String, List<String>> anagrams = newHashMap();


泛型单例工厂

泛型单例工厂模式用于创建不可变但又适合于许多不同类型的对象,由于泛型是通过类型檫除实现的,因此可以给所有必要的类型参数使用单个对象,例子如下:
public interface UnaryFunction<T>{
T apply(T arg);
}
private static UnaryFunction<Object> IDENTITY_FUNCTION =
new UnaryFunction<Object>(){
public Object apply(Object arg){
return arg;
}
}
public static <T> UnaryFunction<T> identityFunction(){
return (UnaryFunction<T>) IDENTITY_FUNCTION;
}


由于泛型是类型檫除的,在运行时对于无状态的泛型参数类型只需要一个泛型单例即可。

泛型递归类型限制

使用泛型可以通过某个包含该类型参数本事的表达式来限制类型参数,如

<T extends Comparable<T>>

读作“针对可以与自身进行比较的每个类型T”,即互比性。

下面的例子是找出列表中实现了Comparable接口的元素的最大值:
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result) > 0){
result = t;
}
return result;
}
}

第28条:利用有限制通配符来提升API的灵活性

由于泛型参数化类型是不可变的,对于任何类型的Type1和Type2而言,List<Type1>既不是List<Type2>的子类型,也不是它的超类型,由此会产生可以将任何对象放进List<Object>中,却只能将字符串放在List<String>中的问题,解决此类问题我们需要使用泛型的通配符。

例子如下:

自定义堆栈的API如下:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
public void pushAll(Iterable<E>  src){
for(E e : src){
push(e);
}
}
public void popAll(Collection<E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
}


上述代码编译完全没有问题,但是如果想完美运行还需要使用泛型通配符。

(1).生产者限制通配符extends:

使用如下的测试数据对pushAll方法进行测试:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = …;
numberStack.pushAll(integers);
在运行时pushAll方法会报参数类型不匹配错误,解决这个问题可以使用限制通配符类型,将pushAll方法修改如下:
public void pushAll(Iterable<? extends E>  src){
for(E e : src){
push(e);
}
}


Iterable<? extends E>的意思是集合元素的类型是自身的子类型,即任何E的子类型,在本例子Integer是Number的子类,因此正好符合此意。

(2).消费者限制通配符super:

使用下面的测试数据对popAll方法进行测试:
Stack<Integer> integerStack = new Stack< Integer>();
Iterable<Number> numbers = …;
integerStack.popAll(numbers);
在运行时popAll方法会报参数类型不匹配错误,解决这个问题可以使用限制通配符类型,将popAll方法修改如下:
public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop());
}
}


Collection<? super E>的意思是集合元素的参数类型是自身的超类型,即任何E的超类,在本例中可以将Integer类型的元素添加到其超类Number的集合中。

上述的两个通配符可以简记为PECS原则,即producer-extends,consumer-super.

(3).无限制通配符?:

对于同时具有生产者和消费者双重身份的对象来说,无限制通配符?更合适,一个交互集合元素的方法声明如下:
public static void swap(List<?> list, int i, list j);


一般来说,如果类型参数只在方法声明中出现一次,就可以使用通配符取代它,如果是无限制的类型参数,就使用无限制通配符?代替。

类型安全的异构容器

第29条:优先考虑类型安全的异构容器

一般情况下,集合容器的只能由固定的类型参数,如一个Set只有一个类型参数表示它的类型,一个Map有两个类型参数表达键和值的类型,但是有些情况下我们需要更多的灵活性,即将容器的键进行参数化而不是将容器参数化,然后将参数化的键提交给容器,来插入或者获取值,用泛型系统来确保值的类型与它的键类型相符。

在JDK1.5之后Class被泛化了,类的类型从字面上来看不再只是简单的Class,而是Class<T>,例如String.class属于Class<String>类型,Integer.class属于Class<Integer>类型。
public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorites(Class<T> type, T instance){
if(type == null){
throws new NullPointerException(“Type is null”);
}
favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
public static void main(String[] args){
Favorites f = new Favorites();
putFavorite(String.class, “java”);
putFavorite(Integer.class, 0xcafebabe);
putFavorite(Class.class, Favorite.class);
String favoriteString = f.getFavorite(String.class);
Int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoritesClass = f.getFavorite(Class.class);
System.out.printf(“%s %x %s%n”, favoriteString, favoriteInteger, favoritesClass);
}
}

程序正常打印出 Java cafebabe
Favorites。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息