Effective java笔记(四),泛型
泛型为集合提供了编译时类型检查。
23、不要在代码中使用原生态类型
声明中具有一个或多个类型参数的类或接口统称为泛型。List<E>
是一个参数化类,表示元素类型为E
的列表。为了提供兼容性,每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。例如,List<E>
的原生态类型为List
。
使用原生态类型将逃避编译时的类型检查,失掉泛型在安全性和表述性方面的优势。出错时(运行时错误)代码所处的位置与包含错误的代码可能相距很远,难以调试。不应该在代码中使用原生态类型。
原生态类型List与参数化类型List的区别:
前者逃避了泛型检查,后者明确告知编译器,它能够持有任意类型的对象
List<String>
是原生态类型List
的子类型,但不是List<Object>
的子类型(泛型不是协变的)。可将List<String>
传递给类型List的参数,但不能传给类型List<Object>
的参数。
如果要使用泛型,但不确定或者不关心实际的类型参数,可使用无限制的通配符类型(<?>
)。
注意:不能将任何元素(除null外)放进Collection<?>
中,而且无法猜测能从中得到哪种类型的对象。若无法接受这些限制可使用泛型方法或有限制的通配符类型。
不要在程序中使用原生态类型例外情况:
在类文字中必须使用原生态类型。
List.class, String[].class
合法,但List<String>.class,List<?>.class
不合法。在参数化类型上使用
instanceof
操作符是非法的(无限制通配符类型除外,其是可具体化的类型),因为泛型信息在运行时会被擦除。
例如:
if(o instanceof Set) {
Set<?> m = (Set<?>) o; //必须转换成通配符类型Set<?>,而不是原生态类型Set
...
}
原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供。Set是参数化类型,表示可以包含任何对象类型的一个集合。Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合。
24、消除非受检警告
泛型编程时,要尽可能的消除每一个非受检警告。消除所有警告可以保证代码的类型安全,避免出现ClassCastException
异常。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的。可以使用@SuppressWarnings("unchecked")
注解来禁止这条警告。SuppressWarnings
注解可以用在任何粒度的级别中,应该在尽可能小的范围中使用SuppressWarnings
注解,永远不要在一个类上使用SuppressWarnings
注解。将一个SuppressWarnings
注解放在return语句中是非法的,因为它不是一个声明,应该声明一个局部变量来保存返回值,并注解其声明。
例如:ArrayList类的toArray方法
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elements, size, a.getClass());
}
//改为
public <T> T[] toArray(T[] a) {
@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
return result;
}
当使用@SuppressWarnings("unchecked")
注解时,使用注释把禁止该警告的原因记录下来。
25、列表优先于数组
数组与泛型的区别:
- 数组是协变的(covariant,表示如果Sub为Super的子类型,则Sub[]就是Super[]的子类型);而泛型是不可变的(因为List不是List的子类型)。所以说数组是有缺陷的。
- 数组是具体化的,运行时检查它们的元素类型约束。而泛型只在编译时强化它们的类型信息,运行时丢弃这些信息。擦除使泛型可以与没有使用泛型的代码随意进行互用。
Object[] obj = new Long[1]; //正确
obj[0] = "I don't fit in"; //出错
List<Object> list = new ArrayList<Long>(); //出错
基于上面的原因,数组和泛型不能很好的混用,不能创建泛型数组。例如,new List<E>[], new List<String>[], new E[]
都是非法的。E、List<E>、List<String>
被称作不可具体化的类型,具体的说,不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数类型为无限制的通配符类型类型,如List<?>
和Map<?,?>
。所以创建无限制通配符类型的数组是合法的。
泛型不能返回它的元素类型数组。
可变参数方法与泛型结合使用会产生警告,因为每当调用可变参数方法时,都会创建一个数组存放参数。除了使用
@SuppressWarnings("unchecked")
注解把它们禁止,并避免在API中混合使用泛型和可变参数外,别无它法。
代码E[] ele = (E[]) new Object[10];
无法再运行时检查转换的安全性,因为元素类型会在运行时从泛型中被擦除。不可具体化的类型的数组转换只能在特殊情况下使用。
数组是协变且可以具体化的;泛型是不可变的且可以被擦除。数组提供了运行时的类型安全,而泛型提供了编译时的类型检查。但数组和泛型不能很好地混用,列表应该优先于数组使用。
26、优先考虑泛型
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 o) {
ensureCapacity();
elements[size++] = o;
}
public E pop() {
if(size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; //【避免内存泄漏】
return result;
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(sx, 2*size + 1);
}
}
}
如上所示,不能创建不可具体化的类型的数组,解决这个问题的办法:
方法一:创建一个Object的数组,并将它转换成泛型数组类型,证明这个转化是否安全,若安全,使用注解消除警告并给出注释。
//The elements array will contain only E instances from push(E)
//This is sufficient to ensure type safety, but the runtime type of the array won't be E[];
//it will always be object[]!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}
方法二:将elements域的类型改为Object[], 并为所用使用Object[]数组中元素的地方进行强转
private Object[] elements;
...
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY]; //产生错误,不能创建泛型数组
}
public E pop() {
if(size == 0)
throw new EmptyStackException();
E result = (E) elements[--size];
elements[size] = null; //【避免内存泄漏】
return result;
}
...
第25条鼓励优先使用列表(List),但有时为了性能或程序的兼容性不得不使用数组。如,HashMap。这时应该特别注意这个问题。
注意:基本数据类型不能用于泛型,这是java泛型系统的局限性。可以使用基本包装类型代替
27、优先考虑泛型方法
静态工具方法尤其适合于泛型化。声明类型参数的类型参数列表,处在方法的修饰符(public、static、final等)及其返回类型之间。
泛型方法的一个显著特点是,无需明确指定类型参数的值,调用泛型构造器时必须指定(jdk1.8后不需要),编译器可通过返回类型和传入的参数类型进行类型推导。
编写一个恒等函数,若每次需要时都重新创建一个,这会很浪费,因为它是无状态的。如果泛型被具体化了,每个类型都需要一个恒等函数,但它们被擦除以后,就只需要一个泛型单例。例如:
public iterface UnaryFunction<T> {
T apply(T arg);
}
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
pubic Object apply(Object arg){ return arg;}
}
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction(){
return (UnaryFunction<T>) IDENTITY_FUNCTION;
}
public static void main(String[] args) {
String[] strings = {"aaa", "bbb", "ccc"};
UnaryFunction<String> sameString = identityFunction();
for(String s : strings) {
System.out.println(sameString.apply(s));
}
}
通过某个包含该类型参数本身的表达式来限制类型参数,被称为递归类型限制。如,<T extends Comparable<T>>
表示针对可以与自身进行比较的每个类型T
。
28、利用有限制通配符来提升API的灵活性
参数化类型是不可变的,对于两个截然不同的类型Type1和Type2,List既不是List的子类型,也不是它的超类型。
有时我们需要的灵活性要比不可变类型所能提供的更多。如:
//泛型类Stack中的方法
public void pushAll(Iterable<E> src) {
for(E e: src)
push(e);
}
//将Integer类型放入Number类型栈中
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...
numberStack.pushAll(integers); //报错,pushAll(Iterable<Number>) cannot be applied to (Iterable<Integer>)
可以通过有限制的通配符类型来解决这个问题,pushAll的输入参数应该为“E的某个子类型的Iterable接口”;类似的方法popAll(从栈中弹出每个元素,并将这些元素添加到指定的集合中)的输入参数应该为“E的某个超类的集合”。
public void pushAll(Iterable<? extends E> src) {
for(E e: src)
push(e);
}
public void popAll(Collection<? super E> dst) {
while(!isEmpty) {
dst.add(pop());
}
}
结论:
为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。若输入参数既是生产者又是消费者,使用严格的类型匹配,不要使用通配符。PECS表示producer-extends, consumer-super,若T是生产者使用<? extends T>
,若T是消费者使用<? super T>
注意:不要用通配符类型作为返回类型
若编译器不能推断你所希望它拥有的类型,可以使用一个显式的类型参数。例如:
import java.util.*;
public class Test {
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
Set<Integer> integers = new HashSet<>();
integers.add(1);integers.add(2);
Set<Double> doubles = new HashSet<>();
doubles.add(3.0);doubles.add(4.0);
Set<Number> numbers = Test.<Number>union(integers, doubles); //显示的类型推断
Set<Number> numbers = Test.union(integers, doubles);//jdk1.8后不报错,之前报错
}
}
Comparable,Comparator始终是消费者,所以Comparable<? super T>
优先于Comparable<T>
,Comparator一样。
T extends Comparable<? super T>
表示T类型不仅能和T类型作比较,还能和T类型的父类型作比较。例如:
public static <T extends Comparable<? super T> T max(List<? extends T> list) {
Iterator<? extends T> i = list.iterator(); //iterator()方法返回T的某个子类型的iterator
T result = i.next();
while(i.hasNext()) {
T t = i.next();
if(t.compareTo(result) > 0) {
result = t;
}
}
return result;
}
如果类型参数只在方法声明中出现一次,就可以使用通配符代替它。有限制的类型参数使用有限制的通配符取代它,无限制的类型参数使用无限制的通配符类型取代它。例如,
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j); //无限制通配符
第二种声明用于swap方法会有一个问题,下面这个实现不能编译:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));//编译报错
}
不能将元素放回到刚刚从中取出的列表中,问题在于list
的类型为List<?>
,你不能把null之外的任何值放到List<?>
中。解决办法是编写一个私有的辅助方法来捕捉通配符类型。如:
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)));
}
通过swapHelper,我们可以导出swap这个比较好的基于通配符的声明到外部API接口
29、优先考虑类型安全的异构容器
泛型常用于容器(集合以及单元素的容器),每个容器只能有固定数目的类型参数,可以通过将类型参数放在键上而不是容器上来避开这一限制。当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作 type token
例如:
import java.util.*;
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)); //用Class的cast方法,防止使用原生态形式的Class对象
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type)); //Class的cast方法
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
System.out.printf("%s %x", favoriteString, favoriteInteger);
}
}
上面代码中,Favorites实例是类型安全的,同时也是异构的(它的所有键都是不同类型),因此Favorites被称作类型安全的异构容器。Class的cast方法:将对象引用动态地转换成Class对象所表示的类型,cast方法是Java的cast操作符的动态模拟,它检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则抛出ClassCastException异常。
注意:由于无限制通配符类型的关系,你可能认为将不能把任何东西放进这个Map中,但事实正好相反。Map<Class<?>, Object>
中通配符类型是嵌套的,这里通配符类型指的不是Map而是键的类型。
Favorites类的局限性:不能用在不可具体化的类型中。如,不能保存List<String>
可以使用有限制的类型令牌(bounded type token)来限制传给方法的类型。如,
public <T extends Annotation> T getAnnotation(Class<T> annotationType)
annotationType表示注解类型的有限制的类型令牌,如果元素有这种类型的注解,该方法就将它返回,若没有返回null。
Class的asSubclass(Class clazz)方法:将class对象转换成其参数表示的类的一个子类,若转换成功,返回它的参数。
Effective java笔记(四),泛型的更多相关文章
- Effective Java笔记一 创建和销毁对象
Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...
- <<Effective Java>> 第四十三条
<<Effective Java>> 第四十三条:返回零长度的数组或者集合,而不是null 如果一个方法的返回值类型是集合或者数组 ,如果在方法内部需要返回的集合或者数组是零长 ...
- Effective java笔记(二),所有对象的通用方法
Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...
- Effective java笔记(一),创建与销毁对象
1.考虑用静态工厂方法代替构造器 类的一个实例,通常使用类的公有的构造方法获取.也可以为类提供一个公有的静态工厂方法(不是设计模式中的工厂模式)来返回类的一个实例.例如: //将boolean类型转换 ...
- effective java笔记之单例模式与序列化
单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...
- effective java笔记之java服务提供者框架
博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...
- Effective Java笔记
chapter 1 java支持四种类型:interface,class,array,primitive(基本类型) chapter 2 创建对象方式: ①构造器 ②静态工厂方法代替构造器:名称可以按 ...
- Effective java笔记(七),通用程序设计
45.将局部变量的作用域最小化 将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. Java允许在任何可以出现语句的地方声明变量(C语言中局部变量要在代码块开头声明),要使 ...
- Effective java笔记(五),枚举和注解
30.用enum代替int常量 枚举类型是指由一组固定的常量组成合法值的类型.在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚 ...
随机推荐
- C#_闭包陷阱
如果匿名方法(Lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到该闭包对象中. 即将for循环中的变量i修改成了引用闭包对象的公共变量i.这样一来,即使代码执行后离开了原局部变量i的 ...
- 编写一个基本的连接池来实现连接的复用&一些工程细节上的优化
package it.cast.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQL ...
- 基于 Asp.Net的 Comet 技术解析
Comet技术原理 来自维基百科:Comet是一种用于web的技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流. 简单的说是一种基于现 ...
- c#开源消息队列中间件EQueue 教程
一.简介 EQueue是一个参照RocketMQ实现的开源消息队列中间件,兼容Mono,具体可以参看作者的文章<分享一个c#写的开源分布式消息队列equeue>.项目开源地址:https: ...
- ASP.NET Core中使用URL重写
ASP.NET Core 1.1 Preview 1 中新增了 URL Rewriting middleware ,终于可以进行 URL 重写了,实际使用体验一下. 首先要将 ASP.NET Core ...
- 《CLR.via.C#第三版》第二部分第8,9章节读书笔记(四)
三种类型的构造方法: 实例构造器(引用类型):实例构造器永远不能被继承(所以方法前没有修饰符):如果类的修饰符为static(sealed和abstract),编译器根本不会在类的定义中生成一个默认构 ...
- EDM制作要点
EDM是Email Direct Marketing的缩写,虽然也是html,但是和我们在网页上使用的html不同,因为安全原因,各大邮箱服务商级邮件客户端都会对邮件内容进行一定程度上的处理,不会按照 ...
- 【VC++技术杂谈005】如何与程控仪器通过GPIB接口进行通信
在工控测试系统中,经常需要使用到各类程控仪器,这些程控仪器通常具有GPIB.LAN.USB等硬件接口,计算机通过这些接口能够与其通信,从而实现自动测量.数据采集.数据分析和数据处理等操作.本文主要介绍 ...
- java 反编译利器JD-JUI
下载地址: http://download.csdn.net/download/suixingbugai/4145221
- AMD规范与CMD规范的区别
AMD规范与CMD规范的区别是什么? 在比较之前,我们得先来了解下什么是AMD规范?什么是CMD规范?当然先申明一下,我个人也是总结下而已,也是网上看到的资料,自己总结下或者可以说整理下而已,供 ...