本文共 9270 字,大约阅读时间需要 30 分钟。
如何正确使用Java泛型
前言 Java 1.5之前是没有泛型的,以前从集合中读取每个对象都必须先进行转换,如果不小心存入集合中对象类型是错的,运行过程中转换处理会报错。有了泛型之后编译器会自动帮助转换,使程序更加安全,但是要正确使用泛型才能取得事半功倍的效果。本文主要从不要使用原生类型,泛型方法,限制通配符,类型安全的异构容器四个部分来说明如何正确使用Java泛型。主要参考资料《Effective Java》(PDF电子版,有需要的朋友可以私信评论)
一、不要使用原生态类型
复制代码
public class RawType_Class {public static void main(String[] args) { List list = new ArrayList<>(); list.add(new Stamp()); list.add(new Coin()); for (Iterator i = list.iterator(); i.hasNext();) { Stamp stamp = i.next(); }}
}
复制代码此时必须使用Cast强转,否则编译会报错,在编译期报错对于开发者来说是我们最希望看到的。但是我们根据提示,增加Cast,好了编译是不会报错了,但是运行时期会报错! Exception in thread "main" java.lang.ClassCastException: ,这就对我们开发者来说大大增加了难度。
复制代码
public class RawType_Class {public static void main(String[] args) { List list = new ArrayList<>(); list.add(new Stamp()); list.add(new Coin()); for (Iterator i = list.iterator(); i.hasNext();) { Stamp stamp = (Stamp) i.next(); }}
}
复制代码由此可见,原生类型是不推荐使用,是不安全的!问1:那为什么Java还要允许使用原生态类型呢?
是为了提升兼容性,Java1.5之前已经存在很多的原生态类型的代码,那么为了让代码保持合法,并且能够兼容新代码,因此Java才对原生态类型支持!
问2:那我们使用List
两者都可以插入任意类型的对象。不严格来说,前者原生态类型List逃避了泛型检查,后者参数化类型List
由于子类型规则的存在,我们可以将List传递给List类型的参数
复制代码
public static void main(String[] args) { List strings = new ArrayList<>(); unsafeAdd(strings, new Integer(1)); String s = strings.get(0);}private static void unsafeAdd(List list, Object o){ list.add(o);}复制代码虽然编译器是没有报错的,但是编译过程会出现以下提示,表明编写了某种不安全的未受检的操作但是我们不能将List传递给List
复制代码
public static void main(String[] args) { List strings = new ArrayList<>(); unsafeAdd(strings, new Integer(1)); String s = strings.get(0);}private static void unsafeAdd(List那么无限制通配类型与原生态类型有啥区别呢?原生态类型是可以插入任何类型的元素,但是无限制通配类型的话,不能添加任何元素(null除外)。
问:那么这样的通配符类型有意义吗?因为你并不知道它到底能加入啥样的元素,但是又美其名曰“无限制”。
不能说没有意义,因为它的出现归根结底是为了防止破坏集合类型约束条件,并且可以根据需要使用泛型方法或者有限制的通配符类型(bound wildcard type)接口某些限制,提高安全性。
复制代码
public static void main(String[] args) {Listl1 = new ArrayList ();List l2 = new ArrayList ();// 输出为true,擦除后的类型为ListSystem.out.println(l1.getClass() == l2.getClass());
}
复制代码结果为true,这是因为:泛型信息可以在运行时被擦除,泛型在编译期有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉。归根结底不管泛型被参数具体化成什么类型,其class都是RawType.class,比如List.class,而不是List.class或List.class事实上,在类文字中必须使用原生态类型,不准使用参数化类型(虽然允许使用数组类型和基本类型),也就是List.class、String[].class和int.class都是合法的,而List.class和List<?>.class不合法
二、泛型方法
1、基本概念 之前说过,如果直接使用原生态类型编译过程会有警告,运行过程可能会报异常,是非常不安全的一种方式。private static Set union(Set s1, Set s2){
Set result = new HashSet(); result.add(s2); return result;}
如果是在方法中使用,为了修正这些警告,使方法变成类型安全的,可以为方法声明一个类型参数。
private static Set union(Set s1, Set s2){
Set result = new HashSet(); result.add(s2); return result;}
static后面的就是方法的类型参数,这样的话三个集合的类型(两个输入参数与一个返回值)必须全部相同。这样的泛型方法不需要明确指定类型参数的值,而是通过判断参数的类型计算类型参数的值,对于参数Set而言,编译器自然知道返回的类型参数E也是String,这就是所谓的类型推导(type inference)
2、泛型单例工厂
有时候我们需要创建不可变但又适合许多不同类型的对象。之前的单例模式满足不可变,但不适合不同类型对象,这次我们可以利用泛型做到这点。复制代码
/**public interface UnaryFunction {
T apply(T arg);
}
复制代码 现在我们需要一个恒等函数(Identity function,f(x)=x,简单理解输入等于返回的函数,会返回未被修改的参数),如果每次需要的时候都要重新创建一个,这样就会很浪费,如果泛型被具体化了,每个类型都需要一个恒等函数,但是它们被擦除后,就只需要一个泛型单例。复制代码
/*** 返回未被修改的参数arg */private static UnaryFunction
复制代码
利用泛型单例编写测试,下面代码不会报任何的警告或错误。复制代码
public static void main(String[] args) {String[] strings = {"hello","world"}; UnaryFunctionsameString = identityFunction(); for (String s: strings) { System.out.println(sameString.apply(s)); } Number[] numbers = {1,2.0}; UnaryFunction sameNumber = identityFunction(); for (Number n: numbers) { System.out.println(sameNumber.apply(n)); } UnaryFunction sameAnotherString = identityFunction(); System.out.println(sameAnotherString.apply(new Stamp()));}
复制代码
返回的都是未被修改的参数public interface Comparable {
public int compareTo(T o);
}
类型参数T定义的类型,可以与实现Comparable的类型进行比较,实际上,几乎所有类型都只能与它们自身类型的元素相比较,比如String实现Comparable,Integer实现Comparable实现compareTo方法
String之间可以相互使用compareTo比较:
String s1 = "a";
String s2 = "b";s1.compareTo(s2);通常为了对列表进行排序,并在其中进行搜索,计算出它的最小值或最大值等,就要求列表中的每个元素都能够与列表中每个其它元素能进行比较,换句话说,列表的元素可以互相比较。往往就需要实现Comparable接口的元素列表。复制代码
/**public class Recursive_Type_Bound {
/** * 递归类型限制(recursive type bound) *>表示可以与自身进行比较的每个类型T,即实现Comparable 接口的类型都可以与自身进行比较,可以查看String、Integer源码 * >类型参数,表示传入max方法的参数必须实现Comparable 接口,才能使用compareTo方法 * @param list * @param * @return */public static > T max(List list) { Iterator iterator = list.iterator(); T result = iterator.next(); while (iterator.hasNext()) { T t = iterator.next(); if (t.compareTo(result) > 0) { result = t; } } return result;}public static void main(String[] args) { List list = Arrays.asList("1","2"); System.out.println(max(list));}
}
复制代码三、有限制的通配符类型
之前提到过的无限制的通配符类型就提到过,无限制的通配符单纯只使用"?"(如Set<?>),而有限制的通配符往往有如下形式,通过有限制的通配符类型可以大大提升API的灵活性。(1)E的某种超类集合(接口):Collection<? super E>、Interface<? super E>、
(2)E的某个子类集合(接口):Collection<? extends E>、Interface<? extends E>
问1:那么什么时候使用extends关键字,什么什么使用super关键字呢?
有这样一个PECS(producer-extends, consumer-super)原则:如果参数化类型表示一个T生产者,就使用<? extends T>,如果表示消费者就是<? super T>。可以这样助记
问2:什么是生产者,什么是消费者
1)生产者:产生T不能消费T,针对collection,对每一项元素操作时,此时这个集合时生产者(生产元素),使用Collection<? extends T>。只能读取,不能写入
2)消费者:不能生产T,只消费使用T,针对collection,添加元素collection中,此时集合消费元素,使用Collection<? super T>,只能添加T的子类及自身,用Object接收读取到的元素
举例说明:生产者
1)你不能在List<? extends Number>中add操作,因为你增加Integer可能会指向List,你增加Double可能会指向Integer。根本不能确保列表中最终保存的是什么类型。换句话说Number的所有子类从类关系上来说都是平级的,毫无联系的。并不能依赖类型推导(类型转换),编译器是无法确实的实际类型的!
2)但是你可以读取其中的元素,并保证读取出来的一定是Number的子类(包括Number),编译并不会报错,换句话说编译器知道里面的元素都是Number的子类,不管是Integer还是Double,编译器都可以向下转型
举例说明:消费者
1)编译器不知道存入列表中的Number的超类具体是哪一个,只能使用Object去接收
2)但是只可以添加Interger及其子类(因为Integer子类也是Integer,向上转型),不能添加Object、Number。因为插入Number对象可以指向List对象,你插入Object,因为可能会指向List对象
注意:Comparable/Comparator都是消费者,通常使用Comparator<? Super T>),可以将上述的max方法进行改造:
复制代码 public static > T max(List<? extends T> list) {Iterator iterator = list.iterator(); T result = iterator.next(); while (iterator.hasNext()) { T t = iterator.next(); if (t.compareTo(result) > 0) { result = t; } } return result;}
复制代码
四、类型安全的异构容器
泛型一般用于集合,如Set和Map等,这些容器都是被参数化了(类型已经被具体化了,参数个数已被固定)的容器,只能限制每个容器只能固定数目的类型参数,比如Set只能一个类型参数,表示它的元素类型,Map有两个参数,表示它的键与值。但是有时候你会需要更多的灵活性,比如关系数据库中可以有任意多的列,如果以类型的方式所有列就好了。有一种方法可以实现,那就是使用将键进行参数化而不是容器参数化,然后将参数化的键提交给容器,来插入或获取值,用泛型来确保值的类型与它的键相符。
我们实现一个Favorite类,可以通过Class类型来获取相应的value值,键可以是不同的Class类型(键Class<?>参数化,而不是Map<?>容器参数化)。利用Class.cast方法将键与键值的类型对应起来,不会出现 favorites.putFavorite(Integer.class, "Java") 这样的情况。
复制代码
/**public class Favorites {
private Map, Object> favorites = new HashMap<>();public void putFavorite(Class type, T instance){ if (type == null) { throw new NullPointerException("Type is null"); } favorites.put(type, type.cast(instance));}public T getFavorite(Class type){ return type.cast(favorites.get(type));}
}
复制代码 Favorites实例是类型安全(typesafe)的,你请求String时,不会返回给你Integer,同时也是异构(heterogeneous)的,不像普通map,它的键都可以是不同类型的。因此,我们将Favorites称之为类型安全的异构容器(typesafe heterogeneous container)。复制代码
public static void main(String[] args) {Favorites favorites = new Favorites(); favorites.putFavorite(String.class, "Java"); favorites.putFavorite(Integer.class, 64); favorites.putFavorite(Class.class, Favorites.class); String favoriteString = favorites.getFavorite(String.class); Integer favoriteInteger = favorites.getFavorite(Integer.class); Class favoriteClass = favorites.getFavorite(Class.class);
// 输出 Java 40 Favorites
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getSimpleName());}
复制代码
Favorites类局限性在于它不能用于在不可具体化的类型中,换句话说你可以保存String,String[],但是你不能保存List,因为你无法为List获取一个Class对象:List.class是错误的,不管是List还是List都会公用一个List.class对象。List list = Arrays.asList("1","2");
List list2 = Arrays.asList(3,4); // 只能选一种,不能有List.class或者List.class favorites.putFavorite(List.class, list2); // favorites.putFavorite(List.class, list) 原文地址转载地址:http://hkjaa.baihongyu.com/