前言
Java 1.5 之前是没有泛型的, 以前从集合中读取每个对象都必须先进行转换, 如果不小心存入集合中对象类型是错的, 运行过程中转换处理会报错. 有了泛型之后编译器会自动帮助转换, 使程序更加安全, 但是要正确使用泛型才能取得事半功倍的效果.
本文主要从不要使用原生类型, 泛型方法, 限制通配符, 类型安全的异构容器四个部分来说明如何正确使用 Java 泛型. 主要参考资料《Effective Java》(PDF 电子版, 有需要的朋友可以私信评论)
一, 不要使用原生态类型
1. 什么是原生态类型?
原生态类型 (Raw type), 即不带任何实际类型参数的泛型名称. 如与 List<E > 对应的原生态类型 List. 不推荐 List list = new ArrayList() 这样的方式, 主要就会丢掉安全性 (为什么不安全呢? 具体请往下看), 应使用 List<MyClass> list = new ArrayList() 明确类型. 或者使用 List<Object>(那么 List 与 List<Object > 有啥区别呢? 具体可以看泛型的子类型规则部分)
2. 为什么不推荐使用原生态类型?
当我们使用原生态类型 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 强转, 否则编译会报错, 在编译期报错对于开发者来说是我们最希望看到的.
但是我们根据提示, 增加 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<Object > 是不是就可以了呢, 两个有啥区别呢?
两者都可以插入任意类型的对象. 不严格来说, 前者原生态类型 List 逃避了泛型检查, 后者参数化类型 List<Object > 明确告诉编译器能够持有任意类型的对象. 但是两个的区别主要是泛型存在子类型规则, 具体请往下看
3. 泛型的子类型规则
子类型规则, 即任何参数化的类型是原生态类型的一个子类型, 比如 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);
- }
编译后就直接报错, 事实上编译器就会自动提示有错误
4. 无限制的通配符类型
使用原生态类型是很危险的, 但是如果不确定或不关心实际的类型参数, 那么在 Java 1.5 之后 Java 有一种安全的替换方法, 称之为无限制的通配符类型 (unbounded wildcard type), 可以用一个 "?" 代替, 比如 Set<?> 表示某个类型的集合, 可以持有任何集合.
那么无限制通配类型与原生态类型有啥区别呢? 原生态类型是可以插入任何类型的元素, 但是无限制通配类型的话, 不能添加任何元素(null 除外).
问: 那么这样的通配符类型有意义吗? 因为你并不知道它到底能加入啥样的元素, 但是又美其名曰 "无限制".
不能说没有意义, 因为它的出现归根结底是为了防止破坏集合类型约束条件, 并且可以根据需要使用泛型方法或者有限制的通配符类型 (bound wildcard type) 接口某些限制, 提高安全性.
5. 泛型的可擦除性
我们先看一下代码, 看看结果:
- 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 不合法
二, 泛型方法
1, 基本概念
之前说过, 如果直接使用原生态类型编译过程会有警告, 运行过程可能会报异常, 是非常不安全的一种方式.
- 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)
2, 泛型单例工厂
有时候我们需要创建不可变但又适合许多不同类型的对象. 之前的单例模式满足不可变, 但不适合不同类型对象, 这次我们可以利用泛型做到这点.
- /**
- * 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()));
- }
返回的都是未被修改的参数
3. 递归类型限制
递归类型限制(recursive type bound): 通过某个包含该类型本身的表达式来限制类型参数, 最普遍的就是与 Comparable 一起使用. 比如 < TextendsComparable<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: 相关泛型术语
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 的意思
来源: https://www.cnblogs.com/jian0110/p/10690483.html