Java - 关于泛型
自Java 1.5开始使用的泛型,泛型给人比较直观的印象是..."尖括号里写了类型我就不用检查类型也不用强转了"。
确实,那先从API的使用者的角度上想问题,泛型还有什么意义?
Discover errors as soon as possible after they are made, ideally at compile time.
泛型提供的正是这种能力。
比如有一个只允许加入String的集合,在没有声明类型参数的情况下,这种限制通常通过注视来保证。
接着在集合中add一个Integer实例,从集合获取元素时使用强转,结果导致ClassCastException。
这样的错误发生在运行时,compiler就爱莫能助了,而声明了类型参数的情况下则是compile-time error。
相比raw type,泛型的优势显而易见,即安全性和表述性。
那么,有了泛型就一定比raw type强吗?
如果类型参数是Object又如何?
这种用法和raw type有什么区别?
如果仅仅通过代码描述,可以说raw type不一定支持哪个类型,而Collection<object>支持任何类型。
正确,但没什么意义。
泛型有种规则叫subtyping rules,比如List是List的子类型,但不是List<object>的子类型。
下面的代码描述了这种情况:
// Uses raw type (List) - fails at runtime!
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
上面的情况导致运行时才能发现错误,而下面这种做法则是编译无法通过:
Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied
to (List<String>,Integer)
unsafeAdd(strings, new Integer(42));
^
而为了应对这种情况Java提供了unbounded wildcard type。
即,对于不确定的类型参数使用‘?’代替。
比如Set<?>可以理解为某个类型的集合。
编码时几乎不会用到raw type,但也有两个例外,而且都和泛型擦除有关。
- must use raw types in class literals.
- it is illegal to use the instanceof operator on parameterized type.
既然如此,为什么Java还保留着raw type用法?
Java 1.5发布的时候Java马上要迎来第一个十年,早已存在大量的代码。
老代码和使用的新特性的代码能够互用至关重要,即 migration compatibility 。
好了,接下来说说使用泛型方面的事情。
关于raw type,在某些IDE中使用了raw type就会出现警告,并提示加上@SuppressWarnings。
比如,eclipse中:
@SuppressWarnings到底有什么作用?
作者给我们的提醒是:要尽量消除这些警告。
提示我们加上@SuppressWarnings是在传达一种信息:无法在运行时检查类型转换的安全性。
而程序员消除这些警告是在传达一种信息:运行时不会出现ClassCastException。
消除警告时优先使用声明类型参数的方式,如果因为某些原因而无法消除警告并且需要证明代码是没有问题时才使用@SuppressWarnings。
如上图所示的那样,@SuppressWarnings可以可以用在变量和方法上,对此我们优先针对更小的粒度。
对于@SuppressWarnings,不忽略,且不盲目。
接着说说subtyping,总觉得因为这一特征,泛型有时反而显得麻烦。
书中原话是covariant(协变)和invaritant。
比如,数组是covariant的,比如Sub是Super的子类,则Sub[]是Super[]的子类。
反之,泛型是invariant的,List不是List的子类。
鉴于这种区别,数组和泛型的难以混合使用。
比如下面这几种写法都是非法的:
new List<E>[]
new List<String>[]
new E[]
下面通过一段代码说明泛型数组为什么非法:
// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
首先假设第一行是合法的。
第二行本身合法,鉴于第一行是合法并且数组是协变的,第三行也是合法的。
鉴于泛型用擦除实现的,即List的运行时类型是List,相应地,List[]的运行时类型是List[],第四行是合法的。
到了第五行则变得矛盾,说好的String类型呢?为什么声明了类型参数还是来了个ClassCastException?
既然如此,索性让第一行产生compile-time error吧,泛型又变得美好了。
举一个例子,比如下面这段代码:
interface Function<T> {
T apply(T arg1, T arg2);
}
static Object reduce(List list, Function f, Object initVal) {
Object[] snapshot;
snapshot = list.toArray();
Object result = initVal;
for (Object e : snapshot)
result = f.apply(result, e);
return result;
}
现在我想把reduce改为泛型方法,于是改成了如下形式:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
E[] snapshot = (E[])list.toArray();
E result = initVal;
for (E e : snapshot)
result = f.apply(result, e);
return result;
}
显然,结果是编译无法通过。
结果是除了在list.toArray上提示加上@SuppressWarnings之外没有任何问题,完全可以正常运行。
不要忽略@SuppressWarnings! 提示我加上@SuppressWarnings是在告诉我:无法在运行时检查类型转换的安全性。
那我应该加上@SuppressWarnings吗?如果加上的话又怎么保证?
其实解决方法很简单,就是不混用数组和泛型,即:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapshot;
synchronized (list) {
snapshot = new ArrayList<E>(list);
}
E result = initVal;
for (E e : snapshot)
result = f.apply(result, e);
return result;
}
这就好了,能用泛型就用。
但换个立场,作为一个提供者而不是使用者,问题还会这样容易吗?
比如描述一个栈:
// 无法通过编译
public class Stack <E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size==0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
// 检查方法略
}
很显然,数组是具体化的,new E[DEFAULTINITIALCAPACITY]上提示Cannot create a generic array of E。
于是我机制地改为(E[])new Object[DEFAULTINITIALCAPACITY]。
这样完全可以通过,然后出现提示让我加上@SuppressWarnings...
我无法忽略,但我可以证明不会出现ClassCastException。
即elements是private的,且没有任何方法可以直接访问它,push和pop的类型都是安全的。
或者我也能将elements的类型改为Object[],并且在pop的时候将元素类型转为E,这样也是可以的。
与其让使用者进行强转,倒不如提供者提供一个安全的泛型。
但不是所有的情况都像上面的例子那样的顺利。
比如我在Stack中增加了一个:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
然后我将Iterable传入Stack中,由于泛型不是协变的,果断来了个compile-time error。
但抛开这些不想,将一堆Integer放到一堆Number又显得那么里所应当。
于是我们就有了bounded wildcard type,关键就是这个bounded。
一个'?'是wildcard,为其加点限制就是bounded wildcard,即:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
相应地,我们再提供一个popAll方法,将pop出来的元素添加到指定集合中。
比如,Stack中的元素必然可以添加到Collection<object>中,也就是:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
对于泛型wildcard的使用,作者指出:PECS,stands for producer-extends, consumer-super.
即,类型参数表示一个生产者则使用<? extends T>,消费者则使用<? super T>。
再举个例子,比如我们要合并两个集合的元素,由于这是生产行为,则声明为:
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);
那么能不能在返回类型中使用通配符?
作者建议尽量不要在返回类型中使用,因为这样会让调用变得复杂。
再来个稍微复杂一些的例子,比如我要在某个参数类型的List中找到最大的元素。
最初的声明为:
public static <T extends Comparable<T>> T max(List<T> list)
返回结果从list参数获得,于是将参数声明改为List<? extends T> list。
那<T extends Comparable>有该如何处理。
以java.util.concurrent中的ScheduledFuture和Delayed为例。
(ps:interface ScheduledFuture extends Delayed 且 interface Delayed extends Comparable.)
即,类型T本身没有实现Comparable,但是他的父类实现了Comparable,于是声明为:
public static <T extends Comparable<? super T> > T max(List<? extends T> list)
最后还有一个有意思的例子,先看代码:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
这段代码是无法通过编译的,因为无法将null以外的元素添加到List<?>中。
当然,如果直接声明一个类型参数就没有问题,但现在假设我们只有使用通配符,并且不能使用raw type。
既然知道通过类型参数可以解决,于是我们可以这样:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
Java - 关于泛型的更多相关文章
- [改善Java代码]Java的泛型是类型擦除的
泛型可以减少强制类型的转换,可规范集合的元素类型,还可以提高代码的安全性和可读性,正是因为有了这些优点,自从Java引入泛型之后,项目的编码规则上便多了一条,优先使用泛型. Java泛型(Generi ...
- Java 中泛型的全面解析(转)
Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在J ...
- Java中泛型 类型擦除
转自:Java中泛型是类型擦除的 Java 泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类 ...
- Java 泛型 Java使用泛型的意义
Java 泛型 Java使用泛型的意义 @author ixenos 直接意义 在编译时保证类型安全 根本意义 a) 类型安全问题源自可复用性代码的设计,泛型保证了类型安全的复用模板 b) 使用复用性 ...
- 跟着刚哥梳理java知识点——泛型(十三)
一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: public class GenericTest { public static void main(String[] a ...
- 【Java】泛型学习笔记
参考书籍 <Java核心技术:卷1> 泛型, 先睹为快 先通过一个简单的例子说明下Java中泛型的用法: 泛型的基本形式类似于模板, 通过一个类型参数T, 你可以"私人定制&qu ...
- [转] Java 的泛型擦除和运行时泛型信息获取
原文链接 https://my.oschina.net/lifany/blog/875769 前言 现在很多程序员都会在简历中写上精通 Java.但究竟怎样才算是精通 Java 呢?我觉得不仅要熟练掌 ...
- Java 容器 & 泛型:五、HashMap 和 TreeMap的自白
Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket Java 容器的文章这次应该是最后一篇了:Java 容器 系列. 今天泥瓦匠聊下 Maps. 一.Ma ...
- 【译】9. Java反射——泛型
原文地址:http://tutorials.jenkov.com/java-reflection/generics.html ===================================== ...
- Java“禁止”泛型数组
Java“禁止”泛型数组 原文:https://blog.csdn.net/yi_Afly/article/details/52058708 1. 泛型定义泛型编程是一种通过参数化的方式将数据处理与数 ...
随机推荐
- Android Performance 性能提升
1. 经常变动的字符串要用 StringBuilder,然后每次变动用 append 方法.而不应该每次创建新的 String. 2. 使用 static final 变量. 3. It's reas ...
- 九、基础正则表达式BRE
1.重要性:简单的说正则表达式就是处理一套字符串的规则和方法,以行为单位对字符串进行处理. 运维工作中,会有大量的访问日志,错误日志,大数据学习正则表达式是不可少的. 2.linux正则表达式,主要是 ...
- 【文文殿下】【HAOI2008】硬币购物
题目描述 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买si的价值的东西.请问每次有多少种付款方法. 数据规模 di,s<=1 ...
- 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)
题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...
- sql—常用函数
COUNT()函数 COUNT()函数返回匹配指定条件的行数 SQL COUNT(column_name) 语法 COUNT(column_name) 函数返回指定列的值的数目(NULL 不计入): ...
- PHP全局变量与SESSION 漏洞(global 与 session)
先看这一段简单的代码 <?php session_start();$_SESSION[‘isadmin’]=’yes’;$isadmin=’no’;echo $_SESSION[‘isadmin ...
- Nginx+Tomcat负载均衡群集
一.Nginx负载均衡原理 目前很多大型网站都应用Nginx服务器作为后端网站程序的反向代理及负载均衡器,提升整个站点的负载并发能力 Nginx负载均衡是通过反向代理实现的 二.部署Tomcat 本案 ...
- 零基础学习Python数据分析
网上虽然有很多Python学习的教程,但是大多是围绕Python网页开发等展开.数据分析所需要的Python技能和网页开发等差别非常大,本人就是浪费了很多时间来看这些博客.书籍.所以就有了本文,希望能 ...
- 初始linux系统--ubuntu
ubuntu操作系统 1. Linux系统组成 Linux内核软件程序用于实现CPU和内存分配进程调度设备驱动等核心操作,以面向硬件为主 外围程序面向用户为主,包括分析用户指令的解释器网络服务程序图 ...
- uml地址栏参数特殊字符处理
转义方法: function URLencode(sStr) { return escape(sStr).replace(/\+/g, '%2B').replace(/\"/g,'%22') ...