Java 1.5以前是没有泛型的,之前从集合中读取每一个对象都必须先进行转换,若是不当心存入集合中对象类型是错的,运行过程当中转换处理会报错。有了泛型以后编译器会自动帮助转换,使程序更加安全,可是要正确使用泛型才能取得事半功倍的效果。java
本文主要从不要使用原生类型,泛型方法,限制通配符,类型安全的异构容器四个部分来讲明如何正确使用Java泛型。主要参考资料《Effective Java》(PDF电子版,有须要的朋友能够私信评论)数据库
原生态类型(Raw type),即不带任何实际类型参数的泛型名称。如与List<E>对应的原生态类型List。不推荐List list = new ArrayList()这样的方式,主要就会丢掉安全性(为何不安全呢?具体请往下看),应使用List<MyClass> list = new ArrayList()明确类型。或者使用List<Object>(那么List与List<Object>有啥区别呢?具体能够看泛型的子类型规则部分)数组
当咱们使用原生态类型List建立一个集合,并往其中放入Stamp类与Coin类,并迭代循环获取List集合中的元素。安全
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强转,不然编译会报错,在编译期报错对于开发者来讲是咱们最但愿看到的。app
可是咱们根据提示,增长Cast,好了编译是不会报错了,可是运行时期会报错! Exception in thread "main" java.lang.ClassCastException: ,这就对咱们开发者来讲大大增长了难度。ide
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才对原生态类型支持!spa
问2:那咱们使用List<Object>是否是就能够了呢,两个有啥区别呢?code
二者均可以插入任意类型的对象。不严格来讲,前者原生态类型List逃避了泛型检查,后者参数化类型List<Object>明确告诉编译器可以持有任意类型的对象。可是两个的区别主要是泛型存在子类型规则,具体请往下看
子类型规则,即任何参数化的类型是原生态类型的一个子类型,好比List<String>是原生态类型List的一个子类型,而不是参数化List<Object>的子类型。
因为子类型规则的存在,咱们能够将List<String>传递给List类型的参数
public static void main(String[] args) { List<String> 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<String>传递给List<Object>类型参数
public static void main(String[] args) { List<String> strings = new ArrayList<>(); unsafeAdd(strings, new Integer(1)); String s = strings.get(0); } private static void unsafeAdd(List<Object> list, Object o){ list.add(o); }
编译后就直接报错,事实上编译器就会自动提示有错误
使用原生态类型是很危险的,可是若是不肯定或不关心实际的类型参数,那么在Java 1.5以后Java有一种安全的替换方法,称之为无限制的通配符类型(unbounded wildcard type),能够用一个“?”代替,好比Set<?>表示某个类型的集合,能够持有任何集合。
那么无限制通配类型与原生态类型有啥区别呢?原生态类型是能够插入任何类型的元素,可是无限制通配类型的话,不能添加任何元素(null除外)。
问:那么这样的通配符类型有意义吗?由于你并不知道它到底能加入啥样的元素,可是又美其名曰“无限制”。
不能说没有意义,由于它的出现归根结底是为了防止破坏集合类型约束条件,而且能够根据须要使用泛型方法或者有限制的通配符类型(bound wildcard type)接口某些限制,提升安全性。
咱们先看一下代码,看看结果:
public static void main(String[] args) { List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); // 输出为true,擦除后的类型为List System.out.println(l1.getClass() == l2.getClass()); }
结果为true,这是由于:泛型信息能够在运行时被擦除,泛型在编译期有效,在运行期被删除,也就是说全部泛型参数类型在编译后都会被清除掉。归根结底无论泛型被参数具体化成什么类型,其class都是RawType.class,好比List.class,而不是List<String>.class或List<Integer>.class
事实上,在类文字中必须使用原生态类型,不许使用参数化类型(虽然容许使用数组类型和基本类型),也就是List.class、String[].class和int.class都是合法的,而List<String>.class和List<?>.class不合法
以前说过,若是直接使用原生态类型编译过程会有警告,运行过程可能会报异常,是很是不安全的一种方式。
private static Set union(Set s1, Set s2){ Set result = new HashSet(); result.add(s2); return result; }
若是是在方法中使用,为了修正这些警告,使方法变成类型安全的,能够为方法声明一个类型参数。
private static <E> Set<E> union(Set<E> s1, Set<E> s2){ Set result = new HashSet(); result.add(s2); return result; }
static后面的<E>就是方法的类型参数,这样的话三个集合的类型(两个输入参数与一个返回值)必须所有相同。这样的泛型方法不须要明确指定类型参数的值,而是经过判断参数的类型计算类型参数的值,对于参数Set<String>而言,编译器天然知道返回的类型参数E也是String,这就是所谓的类型推导(type inference)
有时候咱们须要建立不可变但又适合许多不一样类型的对象。以前的单例模式知足不可变,但不适合不一样类型对象,此次咱们能够利用泛型作到这点。
/** * apply方法接收与返回某个类型T的值 * @param <T> */ public interface UnaryFunction<T> { T apply(T arg); }
如今咱们须要一个恒等函数(Identity function,f(x)=x,简单理解输入等于返回的函数,会返回未被修改的参数),若是每次须要的时候都要从新建立一个,这样就会很浪费,若是泛型被具体化了,每一个类型都须要一个恒等函数,可是它们被擦除后,就只须要一个泛型单例。
/** * 返回未被修改的参数arg */ private static UnaryFunction<Object> IDENTITY_FUNCTION = (Object arg) -> { return arg; }; /** * 泛型方法identityFunction: * 返回类型:UnaryFunction<T> * 类型参数列表;<T> * 忽略强制转换未受检查的警告: * 由于返回未被修改的参数arg,因此咱们知道不管T的值是什么,都是类型安全的 * @param <T> * @return */ @SuppressWarnings("unchacked") public static <T> UnaryFunction<T> identityFunction(){ return (UnaryFunction<T>) IDENTITY_FUNCTION; }
利用泛型单例编写测试,下面代码不会报任何的警告或错误。
public static void main(String[] args) { String[] strings = {"hello","world"}; UnaryFunction<String> sameString = identityFunction(); for (String s: strings) { System.out.println(sameString.apply(s)); } Number[] numbers = {1,2.0}; UnaryFunction<Number> sameNumber = identityFunction(); for (Number n: numbers) { System.out.println(sameNumber.apply(n)); } UnaryFunction<Stamp> sameAnotherString = identityFunction(); System.out.println(sameAnotherString.apply(new Stamp())); }
返回的都是未被修改的参数
递归类型限制(recursive type bound):经过某个包含该类型自己的表达式来限制类型参数,最广泛的就是与Comparable一块儿使用。好比<T extends Comparable<T>>
public interface Comparable<T> { public int compareTo(T o); }
类型参数T定义的类型,能够与实现Comparable<T>的类型进行比较,实际上,几乎全部类型都只能与它们自身类型的元素相比较,好比String实现Comparable<String>,Integer实现Comparable<Integer>
实现compareTo方法
String之间能够相互使用compareTo比较:
String s1 = "a"; String s2 = "b"; s1.compareTo(s2);
一般为了对列表进行排序,并在其中进行搜索,计算出它的最小值或最大值等,就要求列表中的每一个元素都可以与列表中每一个其它元素能进行比较,换句话说,列表的元素能够互相比较。每每就须要实现Comparable接口的元素列表。
/** * @author jian * @date 2019/4/1 * @description 递归类型限制 */ public class Recursive_Type_Bound { /** * 递归类型限制(recursive type bound) * <T extends Comparable<T>>表示能够与自身进行比较的每一个类型T,即实现Comparable<T>接口的类型均可以与自身进行比较,能够查看String、Integer源码 * <T extends Comparable<T>>类型参数,表示传入max方法的参数必须实现Comparable<T>接口,才能使用compareTo方法 * @param list * @param <T> * @return */ public static <T extends Comparable<T>> T max(List<T> list) { Iterator<T> 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<String> 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>,你增长Double可能会指向Integer。根本不能确保列表中最终保存的是什么类型。换句话说Number的全部子类从类关系上来讲都是平级的,毫无联系的。并不能依赖类型推导(类型转换),编译器是没法确实的实际类型的!
2)可是你能够读取其中的元素,并保证读取出来的必定是Number的子类(包括Number),编译并不会报错,换句话说编译器知道里面的元素都是Number的子类,无论是Integer仍是Double,编译器均可以向下转型
举例说明:消费者
1)编译器不知道存入列表中的Number的超类具体是哪个,只能使用Object去接收
2)可是只能够添加Interger及其子类(由于Integer子类也是Integer,向上转型),不能添加Object、Number。由于插入Number对象能够指向List<Integer>对象,你插入Object,由于可能会指向List<Ineger>对象
注意:Comparable/Comparator都是消费者,一般使用Comparator<? Super T>),能够将上述的max方法进行改造:
public static <T extends Comparable<? super T>> T max(List<? extends T> list) { Iterator<? extends T> 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") 这样的状况。
/** * @author jian * @date 2019/4/1 * @description 类型安全的异构容器 */ public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorite(Class<T> type, T instance){ if (type == null) { throw new NullPointerException("Type is null"); } favorites.put(type, type.cast(instance)); } public <T> T getFavorite(Class<T> 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<String>,由于你没法为List<String>获取一个Class对象:List<String>.class是错误的,无论是List<String>仍是List<Integer>都会公用一个List.class对象。
List<String> list = Arrays.asList("1","2"); List<Integer> list2 = Arrays.asList(3,4); // 只能选一种,不能有List<String>.class或者List<Integer>.class favorites.putFavorite(List.class, list2); // favorites.putFavorite(List.class, list)
1)参数化的类型:List<String>
2)实际类型参数:String
3)泛型:List<E>
4)形式类型参数:E
5)无限制通配符类型:List<?>
6)原生态类型:List
7)递归类型限制:<T extends Comparable<T>>
8)有限制的通配符类型:List<? extends Number>
9)泛型方法:static <E> List<E> union()
10)类型令牌:String.class
附2:经常使用的形式类型参数
1)T 表明通常的任何类。
2)E 表明 Element 的意思,或者 Exception 异常的意思。
3)K 表明 Key 的意思。
4)V 表明 Value 的意思,一般与 K 一块儿配合使用。
5)S 表明 Subtype 的意思