前言

  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一起使用。比如<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:相关泛型术语

  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 的意思

如何正确使用Java泛型的更多相关文章

  1. Java泛型学习笔记--Java泛型和C#泛型比较学习(一)

    总结Java的泛型前,先简单的介绍下C#的泛型,通过对比,比较学习Java泛型的目的和设计意图.C#泛型是C#语言2.0和通用语言运行时(CLR)同时支持的一个特性(这一点是导致C#泛型和Java泛型 ...

  2. 初识Java泛型以及桥接方法

    泛型的由来 在编写程序时,可能会有这样的需求:容器类,比如java中常见的list等.为了使容器可以保存多种类型的数据,需要编写多种容器类,每一个容器类中规定好了可以操作的数据类型.此时可能会有Int ...

  3. Java深度历险(五)——Java泛型

      作者 成富 发布于 2011年3月3日 | 注意:QCon全球软件开发大会(北京)2016年4月21-23日,了解更多详情!17 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件 ...

  4. Java泛型学习笔记 - (六)泛型的继承

    在学习继承的时候, 我们已经知道可以将一个子类的对象赋值给其父类的对象, 也就是父类引用指向子类对象, 如: Object obj = new Integer(10); 这其实就是面向对象编程中的is ...

  5. Java泛型-内部原理: 类型擦除以及类型擦除带来的问题

    一:Java泛型的实现方法:类型擦除 大家都知道,Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除.Java的泛型基本上都是在编译 ...

  6. Java泛型-类型擦除

    一.概述 Java泛型在使用过程有诸多的问题,如不存在List<String>.class, List<Integer>不能赋值给List<Number>(不可协变 ...

  7. 浅谈Java泛型之<? extends T>和<? super T>的区别

    关于Java泛型,这里我不想总结它是什么,这个百度一下一大堆解释,各种java的书籍中也有明确的定义,只要稍微看一下就能很快清楚.从泛型的英文名字Generic type也能看出,Generic普通. ...

  8. java 深度探险 java 泛型

    Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JD ...

  9. Java学习笔记(二一)——Java 泛型

    [前面的话] 最近脸好干,掉皮,需要买点化妆品了. Java泛型好好学习一下. [定义] 一.泛型的定义主要有以下两种: 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个 ...

随机推荐

  1. js array 对象

    Javascript 对象: Array 对象:数组 创建方法: 1, var a = new Array() 2,var a = new Array(3) 3,var a = new Array(“ ...

  2. java简单的双色球摇号程序

    import java.util.HashSet; import java.util.Random; import java.util.Set; /** * LotteryClient * @auth ...

  3. socket 套接字服务器端和客户端发送信息

    import socket import threading host='' port=6889 def cilenThred(conn,addr): print("成功接受客户端{}的连接 ...

  4. QEMU KVM Libvirt(12): Live Migration

    由于KVM的架构为 Libvirt –> qemu –> KVM 所以对于live migration有两种方式,一种是qemu + KVM自己的方式,一种是libvirt的方式,当然li ...

  5. 移动端web开发的注意点大总结

    对于手机网站建设,总结了如下几点注意: 1. 安卓浏览器看背景图片,有些设备会模糊. 用同等比例的图片在PC机上很清楚,但是手机上很模糊,原因是什么呢? 经过研究,是devicePixelRatio作 ...

  6. navicat实现Mysql数据备份

    方法/步骤     使用navicat工具连接mysql数据库,这里以navicat for Mysql工具为例.如果数据库在本机,那么连接ip处写localhost即可,如果数据库在其他机器,那需要 ...

  7. 浏览器本地数据库 IndexedDB 基础详解

    一.概述 随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据. 现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的 ...

  8. 吴恩达机器学习笔记56-多元高斯分布及其在误差检测中的应用(Multivariate Gaussian Distribution & Anomaly Detection using the Multivariate Gaussian Distribution)

    一.多元高斯分布简介 假使我们有两个相关的特征,而且这两个特征的值域范围比较宽,这种情况下,一般的高斯分布模型可能不能很好地识别异常数据.其原因在于,一般的高斯分布模型尝试的是去同时抓住两个特征的偏差 ...

  9. [Swift]LeetCode744. 寻找比目标字母大的最小字母 | Find Smallest Letter Greater Than Target

    Given a list of sorted characters letterscontaining only lowercase letters, and given a target lette ...

  10. [Swift]LeetCode838. 推多米诺 | Push Dominoes

    There are N dominoes in a line, and we place each domino vertically upright. In the beginning, we si ...