Effective Java 笔记
创建和销毁对象
1.用静态工厂方法代替构造器
单例模式与序列化
单例有懒汉,饿汉,双重锁,静态内部类,枚举类
单例的要求:构造私有,序列化反序列化唯一,线程实例唯一
在序列化反序列化时,单例类要求提供一个方法readResolve 返回值为单例实例
code:
单例类:
public class Singleton implements Serializable {
private Singleton() {}
public static Singleton getInstance() {
return SingletonBuilder.INSTANCE;
}
/**
* 用于反序列化时保持单例唯一
*/
private Object readResolve() {
return SingletonBuilder.INSTANCE;
}
/**
* 静态内部类 延迟加载单例
*/
static class SingletonBuilder {
private static final Singleton INSTANCE = new Singleton();
}
}
psvm主方法:
ByteArrayOutputStream byteArrayOs = new ByteArrayOutputStream();
ObjectOutputStream objectOs = new ObjectOutputStream(byteArrayOs);
// 对象写入流中
objectOs.writeObject(Singleton.getInstance());
ObjectInputStream objectIs = new ObjectInputStream(new ByteArrayInputStream(byteArrayOs.toByteArray()));
// 从流中读取的对象对比静态内部类单例获取的对象
System.out.println(objectIs.readObject() == Singleton.getInstance());
执行结果:
true
Process finished with exit code 0
6避免创建不必要的对象
要优先使用基本类型而不是装箱基本类型
对于所有对象都通用的方法
10.覆盖equals遵守通用规定
实现高质量equals方法的诀窍:
- 使用==操作符检查"参数是否为这个对象的引用"
性能比比较操作高
- 使用instanceof操作符检查"参数是否为正确的类型"
- 把参数转换成正确的类型
- 对于该类中的每个"关键"(significant)域,检查参数中的域是否与该对象中对应的域相匹配。
13.谨慎的覆盖clone
可以通过序列化完成对象的复制
类和接口
15.使类和成员的可访问性最小化
目的:尽可能地使每个类或者成员不被外界访问
若一个类只被另一个类内部使用,就设计为另一个类的私有嵌套类
公有域的实例域决不能是公有的(静态域亦然)
对于静态域,若只是暴露常量即public static final 则可以但是final域若包含可变对象的引用则不行
让类具有公有的静态final数组域,或者返回这种域的访问方法是错误的
public static final Thing[] VALUES = {...};
解决方法:
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
16.要在公有类而非公有域中使用访问方法
对于包级私有的类,或者是私有的嵌套类,无需提供访问方法,可直接暴露其数据域
class Point {
public double x;
public double y;
}
若类可在它所在的包之外进行访问,就提供访问方法
17.使可变性最小化
为了使类成为不可变,要遵循五条规则:
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 声明所有的域都是final的
- 声明所有的域都为私有的
- 确保对于任何可变组件的互斥访问
18.复合优先于继承
原因:
与方法调用不同的是,继承打破了封装性
复合:
不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例:现有类变成新类的一个组件
如此,新有类中每个实例方法都可以调用被包含的现有类中对应的方法,并返回它的结果,这被称为转发,新类中的方法被称为转发方法。
包装类不适用回调框架
19.要么设计继承并提供文档说明,要么禁止继承
说明可覆盖方法的自用性,即该可覆盖方法调用了哪些其他可覆盖方法
@implSpec标签
构造器决不能调用可被覆盖的方法:若被覆盖的方法依赖构造器方法则初始化失败
构造器调用私有的方法,final方法和静态方法是安全的:这些方法不能覆盖
若在一个为了继承而设计的类中实现Cloneable或者Serializable接口时,其clone和readObject方法都不可以调用可覆盖的方法,不管是直接还是间接。这两个方法在执行时类似构造器。
20.接口优于抽象类
Java提供了两种机制,可用来定义允许多个实现的类型:接口和抽象类
java8为继承引入了缺省方法(default method),即两种机制都允许为某些实例方法提供实现。区别在于,实现抽象类定义的类型遵从单继承,接口则可多实现
接口的优点:
现有的类很容易被更新:实现新的接口
接口时定义mixin(混合类型)的理想选择
接口允许构造非层次结构的类型框架
接口使得安全地增强类的功能成为可能
接口的诠释:
接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)
接口中不允许默认方法覆盖Object方法
!!模拟多重继承:实现了接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上
21.为后代设计接口
java8新增缺省default方法构造,目的是允许给现有的接口添加方法
缺省方法的声明中包括一个缺省实现,这是给实现了该接口但没有实现默认方法的所有类使用。
并非每一个可能的实现的所有变体,始终都可以编写出一个缺省方法:如removeIf方法,在java8中被添加到了Collection接口,这个方法用来移除所有元素,然而此通用方法在SynchronizedCollection实现中会出错。
有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况
慎用接口缺省方法
22.接口只用于定义类型
常量接口模式:将常量定义在接口中,需要的类实现这个接口
public interface PhysicalConstants {
static final double TEST_NUMBER = 3.1414
}
常量接口模式是对接口的不良使用:
使用枚举类,不可实例化的工具类,替代常量接口模式
// Constant utility class
public class PhysicalConstants {
private PhysicalConstants()
public static final double TEST_NUMBER = 3.1414
}
工具类要求客户端用类名修饰这些常量名,若大量利用工具类导出的常量,可通过静态导入机制,避免类名来修饰常量名:
import static xxx.PhysicalConstants.*
public void Test {
return TEST_NUMBER;
}
接口应只被用来定义类型(多态),不该用来导出常量
23.类层次优于标签类
class Figure {
enum Shape { RECTANGLE, CIRCLE };
final Shape shape;
double length;
double width;
}
标签类过于冗长,容易出错且效率低下。
类层次的好处在于,它们可以用来反映类型之间本质上的层次关系
24.静态成员类优于非静态成员类
静态成员类可以访问外围类的所有成员,包括那些声明为私有的成员。
非静态成员类的每个实例都隐含地与外围类的一个 外围实例相关联
如果声明成员类不要求访问外围实例,就要始终把修饰符static放在它的声明中。
当且仅当匿名类出现在非静态的环境中时,他才有外围实例
25.限制源文件为单个顶级类
编译器按源文件的顺序进行编译
若一定要将多个顶级类放进一个源文件中去,就要考虑使用静态成员类,且可将这些类声明为私有
永远不要把多个顶级类或者接口放在一个源文件中
泛型
26. 请不要使用原生态类型
泛型类和泛型接口统称为泛型generic
每一种泛型定义一组参数化的类型parameterized type
每一种泛型都定义一个原生态类型raw type:即不带任何实际类型参数的泛型名称,如List原生类型是List,主要为了泛型出现之前的代码进行兼容。
如果使用原生态类型,就失掉了泛型在安全性和描述性方面的所有优势
原生态类型List和参数化的类型List 的区别:
不严格的说,List逃避了编译器泛型检查,而List<Object>告诉编译器它能够持有任意类型的对象。
虽然可以将List<String>传递给类型List的参数,但是不能将它传给类型List<Object>的参数。
泛型有子类型化subtyping的规则,List<String> 是原生态类型List的一个子类型,而不是参数化类型List<Object>.
若使用像List这样的原生态类型,就会失掉类型安全性,但是若使用像List这样的参数化类型,则不会
可以使用通配符或者有限制的通配符代替原生态类型(不确定或者不关心实际的类型参数)
Set<?>,Set<? extends T>,Set<? super T> 代替Set
不能将任何元素除了null 放到Collection<?>中
例外:必须在类文字class literal 中使用原生态类型即List.class
利用泛型使用instanceof操作符的首选方法:
if (o instanceof Set) {
Set<?> s = (Set<?>) o;
}
确定o是set就转换为通配符类型,这是受检checked转换,不会编译警告
27.消除非受检的警告
泛型编程会遇到许多编译器警告:非受检转换警告(unchecked cast warning),非受检方法调用警告,非受检参数化可变参数类型警告(unchecked parameterized vararg type warning),以及受检转换警告(unchecked conversion warning)
要尽可能地消除每一个非受检警告:避免ClassCastException
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,此时才可以用@SuppressWarnings(“unchecked”)注解来禁止这条警告
应该始终在尽可能小的范围内使用SuppressWarnings注解
如下:声明新的局部变量result,将修饰方法的SuppressWarnings注解修饰局部变量,将禁止非受检警告的范围减到最小
public <T> T[] toArray(T[] a) {
if (a.length < size) {
@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass));
return result;
}
System.arraycopy(elements, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
28.列表优于数组
数组与泛型相比,有两个重要的不同点。
首先,数组是协变的covariant,即若Sub为Super的子类型,那么数组类型Sub[]就是Super[]子类型。
相反,泛型则是可变的invariant:对于任意两个不同的类型Type1和Type2,List既不是List的子类型,也不是List的超类型。
综上数组有缺陷:
Object[] objArray = new Long[1];
objArray[0] = "test"; // Throws ArrayStoreException
然而列表不会
// won't compile 无法通过编译
List<Object> obj = new ArrayList<Long>(); // Incompatible types
obj.add("test")
数组与泛型间第二大区别在于,数组是具体化的reified。因此数组会在运行时知道和强化它们的元素类型。
而泛型通过擦除erasure来实现,泛型只在编译时强化它们的类型信息,并在运行时丢弃或擦除它们的元素类型。擦除就是使泛型可与没有使用泛型的代码随意进行互用(反射).
技术角度说:E,List,List这样的类型称作不可具体化的nonreifiable类型。直观说,不可具体化的类型non-reifiable类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型 无限制的通配符类型
创建无限制通配类型的数组合法,创建泛型数组不合法
混用数组和集合得到编译时报错或警告需要用列表代替数组
29.优先考虑泛型
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object [DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
对于Stack类,应该先被参数化,但没有,可在后面将它泛型化generify,即可将其参数化,而不破坏原来非参数化版本的客户端代码
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;
return result;
}
}
private void ensureCapacity() {
if (size == elements.length) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
其中elements = new E[DEFAULT_INITIAL_CAPACITY];得到错误提示或 警告,每当编写用数组支持的泛型时,都会出现。解决问题有两种方法:
一:直接绕过创建泛型数组的禁令,即创建一个Object的数组,并将它转换成泛型数组类型。
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
错误消除但是编译器产生一条警告,此法不是类型安全的,需要人为保证类型安全,即elements数组放在私有域且保存的唯一元素是通过push方法得到的E类型元素
二:将elements域的类型从E[]改为Object[]
elements = new Object[DEFAULT_INITIAL_CAPACITY];
此时下列代码产生错误
E result = elements[--size];
改为
E result = (E) elements[--size];
此时获得和一一样的警告
一常用但是会引起堆污染(32条)(heap pollution):数组的运行时类型与他的编译时类型不匹配,除非E刚好是Object
每个类型都是都是它自身的子类型:
有限制的类型参数
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
则DelayQueue<Delayed>合法
30.优先考虑泛型方法
方法也可以从泛型中受益,静态工具方法尤其适用于泛型化。如Collections中的所有算法方法都泛型化了 sort binarySearch
public static Set union(Set set1, Set set2) {
// 此处警告类型不安全
Set result = new HashSet(s1);
// 此处警告类型不安全
result.addAll(s2);
return result
}
将以上方法泛型化解决类型安全警告,声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间 即类型参数列表为,返回类型Set
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
以上泛型化的方法其局限性在于参数类型必须完全相同。利用有限制的通配符类型bounded wildcard type可使方法变得更加灵活
而创建一个不可变但又适用于不同类型的对象,可给所有必要的类型参数使用单个对象,如此需要泛型单例工厂,常用于函数对象
递归类型限制recursive type bound 通过某个包含该类型参数本身的表达式来限制类型参数如Comparable接口
public interface Comparable<T> {
int compareTo(T o);
}
public static <E extends Comparable<E>> E max(Collection<E> c);
类型限制<E extends Comparable<E>>,可以读作“针对可以与自身进行比较的每个类型E”
即Comparable声明方法时不知道子类E的存在,通过继承和泛型,动态返回适配子类型
31.利用有限制通配符来提升API的灵活性
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
以上方法编译时正确,然而当src元素类型与Stack的E不匹配时会报错,即Iterable 传入一个Integer
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
报错
解决方法:引入通配符
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
对于pop
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
在以下代码也会报错
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
报错
此时使用<? super E> 即E的某种超类的集合
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
综上<? extends E>用于生产者
<? super E> 用于消费者 **为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型**,若某个输入参数既是生产者又是消费者,则需要严格的类型匹配而非通配符。 **PECS表示producer-extends,consumer-super** ? extends 不能写入任何数据,编译器无法判断写入的具体是哪个子类 ? super 编译器在不知道这个超类具体是什么类,只能返回Object对象,因为Object是任何Java类的最终祖先类。 即参数化类型表示一个生产者T,使用<? extends T>;表示一个消费者T,使用<? super T> 例如Stack实例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E> **不要用通配符类型作为返回类型!** **如果类的用户必须考虑通配符类型,类的API或许就会出错** *** ``` public staticpublic 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)));
}
32.谨慎并用泛型和可变参数
泛型可变参数数组
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
// Heap pollution
objects[0] = intList;
// ClassCastException
String s = stringLists[0].get(0);
}
将值保存在泛型可变参数数组参数中是不安全的
显式创建泛型数组非法,用泛型可变参数声明方法却合法:
Arrays.asList(T...a),Collections.addAll(Collection<? super T> c, T ...elements),以及EnumSet.of(E first,E...rest)都是类型安全的泛型可变参数声明方法
SafeVarargs注解是通过方法的设计者做出承诺,声明这是类型安全的
若方法没有在数组中保存任何值,也不允许对数组的引用转义(这可能导致不被信任的代码访问数组),那么他就是安全的。即,若可变参数数组只用来将数量可变的参数从调用程序传到方法,那么该方法安全:如Arrays.asList
static <T> T[] toArray(T...args) {
return args;
}
这个数组的类型是由传到方法的参数的编译时类型来决定的,因为该方法返回其可变参数数组,他会将堆污染传到调用堆栈上。
static <T> T[] pickTwo(T a, T b, Tc) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
}
执行以下方法报错
public static void main(String[] args) {
String[] attributes = pickTwo("test", "test1", "test2");
}
因为堆污染,toArray返回一个Object[]数组而pickTwo试图返回String[]数组,有一个隐藏的String[]转换,但是转换失败,这是因为从实际导致堆污染的方法处移除了两个级别,可变参数数组在实际参数存入之后没有进行修改
故而允许另一个方法访问一个泛型可变参数数组是不安全的
泛型可变参数方法在下列条件下安全:
- 它没有在可变参数数组中保存任何值
- 它没有对不被信任的代码开放该数组(或者其克隆程序)
解决方法:用一个List参数代替可变参数,同时配合java9的List.of()方法
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
audience = flatten(List.of(friends, romans, countrymen));
总结:
可变参数和泛型不能良好的合作,因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。
33.优先考虑类型安全的异构容器
泛型最常用于集合,如Set,Map<K, V>以及单个元素的容器,ThreadLocal.如此限制了每个容器只能有固定数目的类型参数
有时需要更多的灵活性,即能以类型安全的方式访问所有列:将键key进行参数化而不是容器container参数化,然后将参数化的键提交给容器来插入或者获取值。用泛型系统来确保值的类型与它的键相符。
以下为实例:
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance) ;
public <T> T getFavorite(Class<T> type);
}
示例:
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 faroriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.println(String.format("{%s},{%x},{%s}", faroriteString, favoriteInteger, favoriteClass.getName()));
}
打印:{java},{cafebabe},{Favorites}
Favorite的完整实现:
public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
// 类型转换前做了安全判断
return type.cast(favorites.get(type));
}
}
以上Favorite实例是类型安全的typesafe,当请求String的时候不会返回一个Integer。同时它是异构的heterogeneous:不像普通的映射,它的所有键都是不同类型的,因此我们将Favorites称作类型安全的异构容器typesafe heterogeneous container
其中getFavorite方法是核心,因为putFavorite方法放弃了键和值之间的"类型联系"而getFavorite方法通过Class的cast方法将对象引用动态地转换dynamically cast成了Class对象所表示的类型
Favorites类有两种局限性:
一. 客户端可以通过原生态形式raw form使用Class对象,
确保运行时类型安全的方式是让putFavorite方法检验instance
只需要一个动态的转换
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
二. 它不能用在不可具体化的non-reifiable类型中,即可以保存String,String[]但不能保存List,若保存则程序无法编译,原因在于List.class是个语法错误
Favorites使用的类型令牌type token是无限制的,即可以接收任何Class对象,可以通过有限制类型参数或有限制通配符限制可以表示的类型
若有一个类型为Class<?>的对象,且要将它传给一个需要有限制的类型令牌的方法,此时可将对象Class<?>转换成Class<? extends Annotation>,但这种转换时非受检的
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
return annotationClass.cast(declaredAnnotations().get(annotationClass));
}
而Class类提供了一个安全且动态地执行这种转换的实例方法,称作asSubclass,它将调用它的Class对象转换成用其参数表示的类的一个子类。若转换成功,返回它的参数;失败抛出ClassCastException
以下案例利用asSubclass方法在编译时读取类型未知的注解
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName){
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
枚举和注解
java支持两种特殊用途的引用类型:一种是类,称作枚举类型enum type;一种是接口,称作注解类型annotation type
34.用enum代替int常量
枚举类型是指由一组固定的常量组成合法值的类型,以前使用int枚举模式即private static final int APPLE = 0,缺点在于int枚举模式是编译时常量constant variable ,若常量关联的值发生改变必须重新编译
而枚举类型解决了以前枚举模式的缺点
public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
java的枚举本质上是int值
java枚举类型的基本想法:这些类通过公有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真正的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,只存在声明过的枚举常量。即枚举类型实例受控,它们是单例的泛型化,本质上是单元素的枚举
优点:
- 保证了编译时的类型安全,即传参Apple,则只接受Apple枚举类的常量
- 允许多个枚举类型的同名常量在一个系统中和平共处
- 可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码
枚举类型还允许添加任意的方法和域,并实现任意的接口。它们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
枚举类型有一个静态的values方法,按照声明顺序返回它的值数组。toString方法返回每个枚举值的声明名称
当把一个元素从一个枚举类型中移除时,不会影响传入枚举参数的客户端代码,除非客户端直接引用了被移除的元素
将不同的行为与每个枚举常量关联:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体constant-specific class body中,用具体的方法覆盖每个常量的抽象apply方法,这种方法被称作特定于常量的方法实现constant-specific method implementation
public enum Operation {
PLUS("+"){
@Override
public double apply(double x, double y){return x + y;}
},
MINUS("-") {
@Override
public double apply(double x, double y){return x - y;}
},
TIMES("*") {
@Override
public double apply(double x, double y){return x * y;}
},
DIVIDE("/") {
@Override
public double apply(double x, double y){return x / y;}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public abstract double apply(double x, double y);
@Override
public String toString() {
return symbol;
}
}
本质上PLUS,MINUS,TIMES,DIVIDE都是枚举类型,且是Operation的子类
枚举类型有一个自动产生的valueOf(String)方法,他将常量的名字转变成常量本身。若在枚举类型中覆盖toString方法,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。
要求每个常量都有一个独特的字符串表示法
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
枚举常量创建之后,Operation常量从静态代码块中被放入了到了stringToEnum的映射中。除了编译时常量域外,枚举构造器不可以访问枚举的静态域。这个限制有一个特例:枚举常量无法通过其构造器访问另一个构造器。
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op:" + op);
}
}
如果有多个枚举常量同时共享相同的行为,则要考虑策略枚举:
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay() {
// default
this(PayType.WEEKDAY);
}
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minsWorked, int payRate) {
return payType.pay(minsWorked, payRate);
}
// the strategy enum type
private enum PayType {
WEEKDAY {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
// 分钟计 每天8h
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
枚举类型通常在装载和初始化时需要空间和时间成本
每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举
枚举类型中的常量集并不一定要始终保持不变
35.用实例域代替序数
所有枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置。
切记,永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
public enum Ensemble {
SOLE(1);
private final int num;
Ensemble(int size) {
this.num = size;
}
public int num() {
return num;
}
}
Enum规范中,ordinal是设计用于像EnumSet和EnumMap这种基于枚举的通用数据结构的
36. 用EnumSet代替位域
public class Test {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public void applyStyles(int styles) {...}
}
这种表示法让你用OR位运算将几个常量合并到一个集合中,称作位域bit field:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
EnumSet类能有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,在内部具体的实现上,每个EnumSet内容都表示为位矢量。
如果底层的枚举类型有64个或者更少的元素,整个EnumSet就使用单个long来表示,因此它的性能比得上位域的性能。
使用枚举代替位域:
public class Text {
public enum Style {BOLD, ITALIC}
public void applyStyles(Set<Style> styles) {...}
}
以下将EnumSet实例传递给applyStyles方法的客户端代码。EnumSet提供了丰富的静态工厂来轻松创建集合:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
EnumSet的缺点在于无法创建不可变的EnumSet
正是因为枚举类型要用在集合中,所以没有理由用位域来表示它
37.用EnumMap代替序数索引
有的类会利用ordinal方法来索引数组或列表
public class Plant {
enum LifeCycle {ANNUAL, PERENNIAL, BIENNIAL}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
main方法:
Set<Plant>[] plantsByLifeCycle = new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
以上存在的问题:编译器无法知道序数和数组索引之间的关系
有一种更好的方法就是不用数组充当从枚举到值的映射,而是用EnumMap专门用于枚举键
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
注意:EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息
基于stream的代码
Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle));
这段代码的问题在于它选择自己的映射实现,而不是EnumMap,为了解决这个问题要使用三种参数的Collectors.groupingBy方法,它允许调用者利用mapFactory参数定义映射实现:
Arrays.stream(graden).collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()));
在进行大量使用映射的程序中需要以上优化
当遇到两次索引时可利用EnumMap,(起始阶段, Map(目标阶段, 阶段过渡))
最好不要用序数来索引数组,而要使用EnumMap
若表示的关系时多维的则使用EnumMap<…, EnumMap<…>>
38.用接口模拟可扩展的枚举
对于可伸缩的枚举类型而言,至少有一种具有说服力的用例,即操作码operation code,一般不对枚举进行扩展
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+"){
@Override
public double apply(double x, double y){return x + y;}
},
MINUS("-") {
@Override
public double apply(double x, double y){return x - y;}
},
TIMES("*") {
@Override
public double apply(double x, double y){return x * y;}
},
DIVIDE("/") {
@Override
public double apply(double x, double y){return x / y;}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
虽然BasicOperation不是可扩展的,但是接口类型Operation却是可扩展的,它用来表示API中的操作的接口类型,可以定义另一个枚举类型,实现这个接口,并用这个新类型的实例代替基本类型。
由求幂和求余操作组成
public enum ExtendedOperation implements Operation {
EXP("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
注意,在枚举中,不必像在不可扩展的枚举中所做的那样,利用特定于实例的方法实现(34)来声明抽象的apply方法。因为抽象的方法apply是接口的一部分
不仅可以在任何需要基本枚举的地方单独传递一个扩展枚举的实例,而且除了那些基本类型的元素之外,还可以传递完整的扩展枚举类型,并使用它的元素。
main方法
test(ExtendedOperation.class, x, y);
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {}
}
第二种方法是传入一个Collection<? extends Operation>,这是个有限制的通配符类型,而不是传递一个类对象
main:
test(Arrays.asList(ExtendedOperation.values(), x, y));
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {}
}
它允许调用者将多个实现类型的操作合并到一起,但是放弃了在指定操作上使用EnumSet和EnumMap的功能
接口模拟可伸缩枚举的缺点:无法将实现从一个枚举类型继承到另一个枚举类型。需要将共享功能封装在一个辅助类或者静态辅助方法中
若实现代码不依赖任何状态,就可以将缺省实现放在接口中(20)
虽然无法编写可扩展的枚举类型,却可以编写接口以及实现该接口的基础枚举类型来对它进行模拟
39.注解优先于命名模式
命名模式的缺点:
- 文字拼写错误会导致失败,且没有提示
- 无法确保它们只用于相应的程序元素上
- 它们没有提供将参数值与程序元素关联起来的好方法
注解解决了上述问题
模拟Junit4的test注解
// 运行时有效
@Retention(RetentionPolicy.RUNTIME)
// 修饰方法
@Target(ElementType.METHOD)
public @interface Test {
}
其中@Retention,@Target是修饰注解的元注解meta-annotation
像@Test这样没有参数只是标注被注解的元素的叫标记注解marker annotation
注解不会改变被注解代码的语义,但是使它可以通过工具进行特殊处理
只能用于无参静态方法
private static void test(String className) throws Exception {
Class<?> testClass = Class.forName(className);
int tests = 0;
int passed = 0;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
// 此处执行公有静态方法
m.invoke(null);
passed++;
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
System.out.println(m + "failed:" + t);
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
}
System.out.println(String.format("Passed:%d, Failed: %d%n", passed, tests - passed));
}
有参数的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
// 有限制的类型令牌 允许注解的用户指定任何异常或错误类型
Class<? extends Throwable> value();
}
实例:
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void t1() { // Test should pass
int i = 0;
i = i / i;
System.out.println("t1 instance");
}
@ExceptionTest(ArithmeticException.class)
public static void t2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
System.out.println("t2");
}
@ExceptionTest(ArithmeticException.class)
public static void t3() { // Should fail (no exception)
System.out.println("t3 ");
}
}
private static void test(String className) throws Exception {
Class<?> testClass = Class.forName(className);
int tests = 0;
int passed = 0;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(String.format("Test %s failed: no exception %n", m));
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
// obj instanceof Class
// Class.isInstance(obj) 效果等同上者 obj是否可以转化为这个Class
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.println(String.format("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc));
}
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
}
}
一般进行多值注解需要定义一个数组参数即,Class<? extends Exception>[] value();
而从java8开始,不用数组参数声明一个注解类型而是用@Repeatable元注解对注解的声明进行注解,表示该注解可以被重复地应用给单个元素。
@Repeatable只有一个参数,就是包含注解类型containing annotation type的类对象,他唯一的参数是一个注解类型数组
注意:包含的注解类型必须利用适当的保留策略和目标进行注解,否则无法编译
@Repeatable(ExceptionTestContainer.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
重复注解代替数组值注解之后的代码:
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doubleBad() {}
此时获取注解通过Annotation的方法getAnnotationsByType(Class)获取数组而不是通过getAnnotation方法;且利用isAnnotationPresent检测重复和非重复地注解,必须检查注解类型及其包含的注解类型:如下
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.println(String.format("Test %s failed: no exception %n", m));
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
ExceptionTest[] excTypeTests = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTypeTest : excTypeTests) {
Class<? extends Throwable> excType = excTypeTest.value();
// obj instanceof Class
// Class.isInstance(obj) 效果等同上者 obj是否可以转化为这个Class
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.println(String.format("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc));
}
}
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
40.坚持使用Override注解
在你想要覆盖超类声明的每个方法声明中使用Override注解
由于缺省方法的出现,在接口方法的具体实现上使用Override可以确保签名正确;若接口没有缺省方法,可以选择省略接口方法的具体实现上的Override注解
41.用标记接口定义类型
标记接口marker interface是不包含方法声明的接口,它只是指明一个类实现了具有某种属性的接口。如Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream中(被序列化)
标记接口有两点胜过标记注解。
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。
标记接口类型的存在,允许你在编译时就能捕捉到在使用标记注解的情况下要到运行时才能捕捉到的错误
比如Serializable,序列化时只能传入实现这个接口的实例,否则编译报错,但遗憾的是序列化传参是Object,没有使用上述优点
若使用注解方式,只有运行时尝试序列化一个不可序列化的对象才会报错
- 标记接口胜过标记注解的另一个优点是,它们可以被更加精确地进行锁定
注解类型用目标ElementType.TYPE声明,它就可以被应用于任何类或者接口。而一个标记只适用于特殊接口的实现,此时将它定义为标记接口则确保所有被标记的接口都是该唯一接口的子类型
如Set接口是这种有限制的标记接口restricted marker interface 它只适用于Collection子类型
标记注解胜过标记接口的最大优点在于,它们是更大的注解机制的一部分
因此标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性
若标记是应用于任何程序元素而不是类或者接口则使用注解否则优先使用标记接口(标记是广泛使用注解框架的一个组成部分则选择标记注解)
若你发现自己在编写的目标为ElementType.TYPE的标记注解类型,就要花时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适
Lambal和Stream
java8中,增加了函数接口function interface,Lambda和方法引用method reference,使得创建函数对象function object变得容易,同时,还增加了StreamAPI
42.Lambda优先于匿名类
java8以前函数对象的主要方式是通过匿名类anonymous class(单个抽象方法的接口作为函数类型,它们的实例称作函数对象,表示函数或者要采取的动作)
匿名类满足了传统的面向对象的设计模式对函数对象的需求,即策略模式Strategy
java8形成了"带有单个抽象方法的接口是特殊的,值得特殊对待"的观念,这些接口现在被称作函数接口functional interface,java允许利用Lambda表达式创建这些接口的实例。
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
参数类型和返回值类型以及Lambda的类型都没有出现在代码中,编译器利用类型推导type inference的过程,根据上下文推断出这些类型
故而删除所有Lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰
编译器是从泛型获取到得以执行类型推导的大部分类型信息的
Collections.sort(words, comparingInt(String::length));
java8在List接口中添加的sort方法更简短
words.sort(comparingInt(String::length));
利用Lambda优化34条的Operation枚举类型
给每个枚举常量的构造器传递一个实现其行为的Lambda,构造器将Lambda保存在一个实例域中,apply方法再将调用转给Lambda
public enum Operation {
PLUS("+", (x, y) -> x + y),
...;
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
DoubleBinaryOperator接口表示带有两个double参数的函数,并返回一个double结果
注意:传入枚举构造器的参数是在静态的环境中计算的,因而枚举构造器中的Lambda无法访问枚举的实例成员
Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。对于Lambda而言,一行是最理想的,三行是合理的极限
Lambda无法获得对自身的引用,Lambda中this是指外围实例而匿名类中,this指匿名类实例
尽可能不要序列化一个Lambda或者匿名类实例,若想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例
千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例
43.方法引用优先于Lambda
java提供了生成比Lambda更简洁函数对象的方法:方法引用method reference
以下,当这个键不在映射中时,将数字1和键关联起来;或者当这个键已经存在,就负责递增该关联值:
map.merge(key, 1, (count, incr) -> count + incr);
merge方法,是java8 Map接口中添加的,若指定的key没有映射,该方法就会插入指定值;若映射存在,merge方法就会将指定函数应用到当前值count和指定值incr上,并用结果覆盖当前值
java8 Integer(以及所有其他的数字化基本包装类型)提供了sum的静态方法
map.merge(key, 1, Integer::sum);
更为简洁
若Lambda太长,或者过于复杂,还有另一种选择:从Lambda中提取代码,放到一个新的方法中,并用该方法的一个引用代替lambda
许多方法引用都指向静态方法,但其中4种没有这么做。其中两个是有限制bound和无限制unbound的实例方法引用。在有限制的引用中,接收对象是在方法引用中指定的。有限制的引用本质上类似于静态引用:函数对象与被引用方法带有相同的参数。在无限制的引用中,接收对象是在运用函数对象时,通过在该方法的声明函数前面额外添加一个参数来指定的。无限制的引用经常用在流管道Stream pipeline中作为映射和过滤函数。最后还有两种构造器引用,分类针对类和数组。构造器引用充当工厂对象
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态 | Integer::parseInt | str -> Integer.parseInt(str) |
有限制 | Instant.now()::isAfter | Instant t = new Instant.now(); i->t.isAfter(i) |
无限制 | String::toLowerCase | str -> str.toLowerCase |
类构造器 | TreeMap<K, V> :: new | ()->new TreeMap<K, V> |
数组构造器 | int[]::new | len -> new int[len] |
只要方法引用更加简洁,清晰,就用方法引用;若方法引用并不简洁,就坚持使用Lambda
44.坚持使用标准的函数接口
函数接口取代模板方法模式
只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口
许多标准的函数接口都提供了有用的默认方法,如Predicate接口提供了合并断言的方法
函数接口有43个接口,其中有6个基础接口,可以推断出其余接口。基础接口作用于对象引用类型。
Operator接口代表其结果与参数类型一致的函数
Predicate接口代表带有一个参数并返回一个boolean的函数
Function接口代表其参数与返回类型不一致的函数
Supplier接口代表没有参数并且返回或提供一个值的函数
Consumer代表的是带有一个函数但不返回任何值的函数,消费
接口 | 函数签名 | 范例 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function<T, R> | R apply(T) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
如今大多数标准函数接口都只支持基本类型,千万不要用带包装类型的基础函数接口来代替基本函数接口
若你所需要的函数接口与Comparator一样具有一项或者多项以下特征,则需要考虑自己编写专用的函数接口,而不是使用标准的函数接口:
- 通用,并且将受益于描述性的名称
- 具有与其关联的严格的契约
- 将受益于定制的缺省的方法
@FunctionalInterface注解标注自定义函数接口
必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注
45.谨慎使用Stream
java8的Stream API,简化了串行或并行的大批量操作。这个API提供了两个关键抽象:Stream代表数据元素有限或无限的顺序,Stream pipeline流管道则代表这些元素的一个多级计算。Stream中的元素可能来自任何位置。常见的来源包括集合,数组,文件,正则表达式模式匹配器,伪随机数生成器,以及其他Stream。Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int,long,double
一个Stream pipeline中包含一个源Stream,接着是0或多个中间操作intermediate operation和一个终止操作terminal operation。每个中间操作都会对Stream进行转换,从一个Stream转换成另一个Stream,其元素类型可能与输入的Stream一样,也可能不一样(filter,map中间操作)。终止操作会在最后一个中间操作产生的Stream上执行一个最终的运算(foreach,collect,findFirst)
Stream pipeline通常是lazy懒加载:直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算,无限Stream成为可能。注意,没有终止操作的Stream pipeline将是一个静默的无操作指令,故而不要忘记终止操作。
Stream API是流式的fluent:所有包含pipeline的调用可以链接成一个表达式。
默认情况下,Stream pipeline是按顺序运行的。要使pipeline并发执行,只需在该pipeline的任何Stream上调用parallel方法即可,通常不建议这样做(48条)
java8 Map新增方法compute,computeIfAbsent,computeIfPresent
computeIfAbsent:在映射中查找键,存在返回关联值,不存在对该键运用指定的函数对象算出一个值,将这个值与键关联起来并返回计算得到的值,此方法简化了将多个值与每个键关联起来的映射实现
滥用Sream会使程序代码难以读懂和维护
public class Anagrams {
public static void main(String[] args) {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
} catch (Exception e) {
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
不好的示例:
public class Anagrams {
public static void main(String[] args) {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
Collectors.groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ":" + group)
.forEach(System.out::println);
} catch (Exception e) {
}
}
}
好的示例:
public class Anagrams {
public static void main(String[] args) {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(Collectors.groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ":" + g));
} catch (Exception e) {
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
在try-with-resources中打开词典文件,获得一个包含了文件中所有代码的Stream。Stream变量命名为words,该Stream中的每个元素均为单词。这个Stream中的pipeline没有中间操作;它的终止操作将所有的单词集合到一个映射中,按照它们的字母排序形式对单词进行分组。随后在映射的values视图中打开一个新的Stream<List<String>>.Stream过滤将所有单词长度小于minGroupSize的单词都去掉了
在没有显式类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要
在Stream pipeline中使用helper方法,对于可读性而言,比在迭代化代码中使用更为重要
最好避免利用Stream来处理char值,因为Java不支持基本类型的char Stream,char会自动转换为int基本类型,除非强制转换为char
重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用
以下工作只能通过代码块,而不能通过函数对象来完成:
- 从代码块中,可以读取或者修改范围内的任意局部变量;Lambda中只能读取final或者有效地final变量,且不能修改任何local变量
- 从代码块中,可以从外围方法中return, break或continue外围循环,或者抛出该方法声明要抛出的任何受检异常
Stream使用场景:
- 统一转换元素的序列
- 过滤元素的序列
- 利用单个操作如添加,连接或计算其最小值 合并元素的顺序
- 将元素的序列存放到一个集合中,比如分组
- 搜索满足某些条件的元素的序列
若不确定用Stream还是迭代,那么就两种都试试,选择好理解,简洁的
46.优先选择Stream中无副作用的函数
forEach是显式迭代,不适合并行
forEach操作应该只用于报告Stream计算的结果,而不是执行计算
静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipeline的可读性
import static java.util.stream.Collectors.*;
Collectors中的大部分方法是为了便于将Stream集合到映射中而存在,每个Stream元素都有一个关联的键和值,多个Stream元素可以关联同一个键。
最简单的映射收集器是toMap(keyMapper, valueMapper),它带有两个函数,其中一个是将Stream元素映射到键,另一个是将它映射到值。
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(
toMap(Object::toString, e -> e));
此时多个Stream元素映射到同一个键,pipeline会抛出IllegalStateException异常并终止
比toMap更复杂的是groupingBy方法,提供更多处理此类冲突的策略,如给toMap提供键,值,合并函数即BinaryOperator,V为映射的值类型。
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
以上比较器使用了静态工厂方法maxBy,从BinaryOperator静态导入的,该方法将Comparator转换成一个BinaryOperator,用于计算指定比较器产生的最大值。
三个参数的toMap还有一种用途,即生成一个收集器,有冲突时保留最后更新的
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
toMap的最后一种形式是带有第四个参数,映射工厂,使用时要指定特殊的映射实现,如EnumMap或TreeMap
toMap的前三种还有另外的变换形式,命名为toConcurrentMap,能并行运行,并生成ConcurrentHashMap实例
除了toMap方法,Collectors还提供了groupingBy方法,根据分类函数将元素分组,分类函数带有一个元素,并返回所属的类别。这个类别就是元素的映射键。
words.collect(groupingBy(word -> alphabetize(word)));
键为alphabetize(word),值为包含word元素的List集合
两个参数的groupingBy,第二个参数为指定的一个下游收集器downstream collector
下游收集器,指定返回的集合类型,toSet()或toCollection(collectionFactory)允许创建存放各元素类别的集合 如toCollection(HashSet::new))
下游收集器,指定返回的值而不是列表的映射如下
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
groupingBy的第三个版本:
第二个参数指定映射工厂mapFactory
第三个参数指定下游收集器
注意:groupingByConcurrent方法提供了groupingBy所有三种重载的变体,可并发运行生成ConcurrentHashMap实例
通过在Stream上的count方法可以替代collect(counting()),这个属性还有15种Collectors方法。其中包含9种方法其名称以summing,averaging和summarizing开头(相应的Stream基本类型上就有相同的功能)。它们还包括reducing,filtering,mapping,flatMapping和collectingAndThen方法。
Collectors的joining方法,只在CharSequence实例的Stream中操作,如字符串。(可被String.join()替代),它以参数的形式,返回一个简单地合并元素的收集器。其中一种参数形式带有一个delimiter分解符的CharSequence参数,它返回一个连接Stream元素并在相邻元素之间插入分隔符的收集器。注意分隔符和字符串本身有的字符歧义。三个参数的joining除了分隔符delimiter还有前缀prefix,后缀suffix。
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix)
最重要的收集器工厂是toList,toSet,toMap,groupingBy,joining
47.Stream要优先用Collection作为返回类型
java8新增的Stream本质上导致给序列化返回的方法选择适当返回类型的任务变得复杂。
Stream并没有淘汰迭代:要编写优秀的代码必须巧妙地将Stream与迭代结合起来使用
若一个API只返回Stream,则无法直接用for-each循环遍历。因为Stream接口只在Iterable接口中包含了唯一一个抽象方法,Stream对于该方法的规范也适用于Iterable的。
for (ProcessHandle ph : ProcessHandle.addProcesses()::iterator) {
statement
}
以上代码编译器报错
以下不报错
// 为了使代码能够进行编译,必须将方法引用转换成适当参数化的Iterable
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.addProcesses()::iterator) {
statement
}
以上代码使用适配器方法进行优化:
在适配器方法中没有必要进行转换,因为java的类型引用在这里派上了用场
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
利用以上适配器可以利用for-each遍历任何Stream:
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
statement
}
反过来,想要利用Stream pipeline处理序列,面对Iterable也需要适配器
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
// Spliterator用于并行遍历元素
// public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) 是否并行
return StreamSupport.stream(iterable.spliterator(), false);
}
而对于想要编写Stream pipeline和Iterable的人来说,应该考虑返回Collection接口
Collection接口是Iterable的一个子类型,它有一个stream方法,因此提供了迭代和stream访问。
对于公共的,返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型
数组也通过Arrays.asList和Stream.of方法提供了简单地迭代和stream访问
千万别在内存中保存巨大的序列,将它作为集合返回即可
Collection相比于Stream或Iterable作为返回类型的缺点:Collection有一个返回int类型的size方法,它限制返回的序列长度为Integer.MAX_VALUE或者2e31 -1 。
48.谨慎使用Stream并行
stream并行即调用parallel方法,要考虑安全性和活性失败
import static java.math.BigInteger.*;
public static void main(String[] args) {
final BigInteger TWO = new BigInteger("2");
// 返回2的幂为素数 然后减1 且是素数的前20条数据
primes.map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
// 根据函数迭代产生初始为TWO,无限素数组成的stream流
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
对以上stream调用parallel方法导致活性失败,程序无法打印
即便是最佳环境下,若调用parallel的Stream源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能,更糟糕的是默认的并行策略在处理limit的不可预知性。
千万不要任意地并行Stream pipeline 其结果可能是灾难性的
**在Stream上通过并行获得的性能,最好是通过ArrayList,HashMap,HashSet和ConcurrentHashMap实例,数组,int范围和long范围等。**这些数据结构的共性是,都可以被精确,轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松。
Stream类库用来执行这个任务的抽象是分割迭代器spliterator,它是由Stream和Iterable中的spliterator方法返回的。
这些数据结构共有的另一项重要特性是,在进行顺序处理时,它们提供了优异的引用局部性locality shof reference:序列化的元素引用一起保存在内存中。而那些对象是分散保存在内存中,而基本类型数组相邻地保存在内存中
Stream pipeline的终止操作本质上也影响了并发执行的效率(大量的工作在终止操作符完成而不是在pipeline中完成,则并行受到限制)。并发的最佳终止操作是做减法reduction,用一个stream的reduce方法,将所有从pipeline产生的元素都合并在一起,或者预先打包像min,max,count,sum这类方法。驟死式操作如anyMatch,allMatch,和noneMatch也都可以并行。由Stream的collect方法执行的操作,都是可变的减法,不是并行的最好选择,因为合并集合的成本高
若是自己编写Stream,Itereable或者Collection实现,且想要得到适当的并行性能,就必须覆盖spliterator方法
并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预料的行为如安全性失败
程序中所有并行Stream pipeline都是在一个通用的fork-join池中运行的。只要有一个pipeline运行异常,都会损害到系统中其他不相关部分的性能
在适当的条件下,给Stream pipeline添加parallel调用,确实可以在多处理器的情况下实现近乎线性的倍增
parallel有效地例子:
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
如果要并行一个随机数的Stream,应该从SplittableRandom实例开始,而不是从ThreadLocalRandom或实际上已经过时的Random开始
ThreadLocalRandom只用于单线程,它将自身当做一个并行的Stream源运用到函数中
Random在每个操作都进行同步,扼杀了并行的优势
方法
49.检查参数的有效性
没有验证参数的有效性,可能导致违背失败原子性failure atomicity
在java7中增加的Objects.requireNonNull方法比较灵活且方便,因此不必再手工进行null检查,这个方法返回其输入,且能指定异常详情
java9增加了检查范围的设施Objects:checkFromIndexSize,checkFromToIndex和checkIndex
非公有的方法通常应该使用断言assertion来检查它们的参数
private static void sort(long a[], int offset, int length) {
assert a != null; // 为true则执行下面 否则抛出AssertionError异常
}
编写方法和构造器时考虑它的参数限制
50.必要时进行保护性拷贝
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
如下例:
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "after" + end);
}
this.start = start;
this.end = end;
}
public Date getStart() {
return this.start;
}
public Date getEnd() {
return this.end;
}
}
以上的类因为Date是可变的导致客户端可以修改其内容
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYead(78); // 导致开始时间大于结束时间,不合理
从java8开始利用Instant或者LocalDateTime或者ZonedDateTime代替Date,因为Instant是不可变的
Date已经过时了,不应该在新代码中使用
为了保护Period实例的内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝defensive copy是必要的
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
...
}
注意:保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象,这样做可以避免在危险阶段window of volnerability 期间从另一个线程改变类的参数:危险阶段是指从检查参数开始,直到拷贝参数之间的时间段
注意非final类如Date不要轻易使用.clone方法进行保护性拷贝。因为不能保证clone方法一定返回类为Date的对象:它有可能返回处于恶意目的设计的不可信子类的实例。为了阻止这种攻击,对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝
对于Period除了构造器同时需要使它返回可变内部域的保护性拷贝
public Date getStart() {
return new Date(this.start.getTime());
}
此时Period是不可变的了,不管如何操作,start都不能晚于end,约束不会被破坏
此时访问方法的进行保护性拷贝允许使用clone方法,因为Period内部的Date对象的类型是Date,不会是其他类型
在把内部数组返回给客户端之前进行保护性拷贝或返回给客户端该数组的不可变视图
只要有可能都应该使用不可变的对象作为对象内部的组件
51.谨慎设计方法签名
谨慎地选择方法的名称
不要过于追求提供便利的方法,方法太多影响使用
当一项操作被经常使用时,考虑提供快捷方式shorthand 如果不能确定,最好不要提供快捷方式
避免过长的参数列表,目标是4个参数或者更少,相同类型的长参数序列格外有害,有三种技巧可以缩短过长的参数列表。
- 第一种是把一个方法分解成多个方法,同时提升它们的正交性,如List的subList,indexOf,lastIndexOf方法;
- 第二种是创建辅助类helper class,用来保存参数的分组。这些辅助类一般为静态成员类。如元组形式的参数分组(a,b,c)
- 第三种是使用构建者模式Builder
对于参数类型,要优先使用接口而不是类,LSP准则
对于boolean参数,要优先使用两个元素的枚举类型
Thermonmeter.newInstance(TemperatureScale.CELSIUS)
比
Thermonmeter.newInstance(true)
更有用
52.慎用重载
public static String classify(Set<?> s) {}
public static String classify(List<?> l) {}
public static String classify(Collection<?> c) {}
classify(list),classify(set),classify(collection)
以上三种重载方法在传入list,set,collection(Collection<?>的形式传入)集合时只会执行参数为Collection的方法
对于重载方法的选择是静态的,而对于被覆盖的方法的选择是动态的
应该避免胡乱使用重载机制,安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法
public static String classify(int ...i) {}
public static String classify(Integer ...i) {}
classify(1, 2) // 编译报错
对于可变参数的重载,基本类型和包装类型数组传参需要用 classify(new int[]{1, 2}) 确定类型
要调用哪个重载方法是在编译时做出的决定
对于重载各个参数的调用顺序如下:
JVM在重载方法中,选择合适的目标方法的顺序:
1.精确匹配
2.若是基本数据类型,自动转换成更大表示范围的基本类型
3.通过自动拆箱和装箱
4.通过子类向上转型继承路线依次匹配
5.通过可变参数匹配
注意:null可以匹配任何类对象,在查找方法时是从最底层子类依次向上查找,如参数是Integer的重载方法,Integer是Object的子类故而null匹配Integer的重载方法,若还有String的重载方法则报错,因为null不知选择哪一个匹配
对于子父类关系中的重载方法,重载在编译时可以根据规则知道调用哪个目标方法,故而重载又被称为静态绑定
让更具体化的重载方法把调用转发给更一般化的重载方法
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
不要在相同的参数位置调用带有不同函数接口的方法
对于多个具有相同参数数目的方法来说,应尽量避免重载方法
53.慎用可变参数
可变参数一般称为variable arity method,本质是一个接收数组
static int min(int ... args) {
if (args.length == 0) {
throw new IllegalArgumentException("Too few arguments");
}
int min = args[0];
for (int i = 1; i < args.length; i++) {
if (args[i] < min) {
min = args[i];
}
}
return min;
}
通过一个指定类型的正常参数,和一个这种类型的可变参数进行优化
static int min(int firstArg, int ... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs) {
if (arg < min) {
min = arg;
}
}
return min;
}
以上方法必须至少提供一个参数将上一个方法的运行时异常优化到编译时异常
54.返回零长度的数组或者集合,而不是null
private final List<Cheese> cheesesInStock = ...;
public List<Cheese> getCheese() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
对于以上方法的调用必须为判断非空写额外的代码
if (cheeses != null && cheeses.contains(Cheese.STILTON)) {
...
}
对于一个返回null而不是零长度数组或者集合的方法,每次用到都要非空判断。虽然null避免了分配零长度集合的开销;但是不需要分配它们也可以返回它们
返回可能的零长度集合的典型代码
public List<Cheese> getCheeses() {
return new ArrayList<>(cheesesInStock);
}
若认为分配零长度的集合损害了程序的性能,可以通过重复返回同一个不可变的零长度集合,避免了分配的执行,因为不可变对象可以被自由共享。
public List<Cheese> getCheeses() {
// 若返回空集合使用emptySet,空映射emptyMap
return cheesesInStock.isEmpty() ? Collections.emptyList();
}
数组也一样,永远不要返回null,而是返回零长度的数组
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
永远不要返回null,而不返回一个零长度的数组或者集合
55.谨慎返回optional
不能返回值的方法除了抛出异常,返回null还有返回Optional。Optional 类代表的是一个不可变的容器,它可以存放单个非null的T引用,或者什么内容都没有。不包含任何内容的optional称为空empty。非空的optional中的值称作存在present。optional本质上是一个不可变的集合,最多只能存放一个元素。Optional 没有实现Collection接口,但原则上可以
public void ifPresent(Consumer<? super T> consumer)
若存在则消费
public boolean isPresent()
判断存的value值是否存在,存在才可以get
public static<T> Optional<T> empty()
返回空value的Optional对象
public static <T> Optional<T> of(T value)
设置value,不能传null
public static <T> Optional<T> ofNullable(T value)
设置value,可以传null 返回空empty的Optional 需要isPresent判断存在
public T get()
获取value,若为null抛出异常
public T orElse(T other)
value不存在则返回默认值other
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X
value不存在则抛出指定的异常 通过Supplier生产异常
public T orElseGet(Supplier<? extends T> other)
通过生产者指定默认值,即必要时使用默认值的开销 懒加载
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty()) {
return Optional.empty();
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return Optional.of(result);
}
永远不要通过返回Optional的方法返回null
stream的许多终止操作都返回optional
Optional本质上与受检异常相类似
若Optional的方法不能返回值时,可以指定默认值
String lastWordInLexicon = max(words).orElse("");
或者抛出适当的异常
E e = max(es).orElseThrow(TemperTantrumException::new);
当获取缺省值的开销高时,使用orElseGet方法,通过Supplier提供缺省值,必要时创建
java8 Optional的filter,map,flatMap和ifPresent和java9 or,ifPresentOrElse等特殊方法用于特殊的用例
以上都无法满足可以使用isPresent方法自己进行代码逻辑编写
使用Stream编程,经常会遇到Stream<Optional>,使用java8,获取非空optional中所有元素的Stream 如下
streamOfOptionals.filter(Optional::isPresent).map(Optional::get);
在java9中,Optional还配有一个stream()方法,这个方法是一个适配器,若optional中有一个值,它就将Optional变成包含一个元素的Stream;若optional为空,则其中不包含任何元素。这个方法结合Stream的flatMap方法可以简洁地取代以上代码片段:
// flatMap 一对多,流的扁平化处理,将每个元素流化生成子stream 再进行合并
streamOfOptionals.flatMap(Optional::stream);
但是并非所有的返回类型都受益于optional的处理方法。容器类型包括集合,映射,Stream,数组和optional,都不应该包装在optional中
如果无法返回结果并且当没有返回结果时客户端必须执行特殊的处理,那么就应该声明该方法返回optional
Optional需要额外的开销,为此,基本类型提供了类似Optional的方法,如OptionalInt (value == 0 则为empty)OptionalLong(0)和OptionalDouble(Double.NaN)
永远不应该返回基本包装类型的optional,小型的基本类型如Boolean,Byte,Character,Short,Float除外
几乎永远都不适合用optional作为键,值,或者集合或数组中的元素
若注重性能,则最好返回一个null,或者抛出异常。最后尽量不要将optional用作返回值以外的任何其他用途
56.为所有导出的API元素编写文档注释
为了正确地编写API文档,必须在每个被导出的类,接口,构造器,方法和域声明之前增加一个文档注释
方法的文档注释应该简洁地描述出它和客户端之间的约定
文档注释在源代码和产生的文档中都应该是易于阅读的
同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述
当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数
当为枚举类型编写文档时,要确保在文档中说明常量
为注解类型编写文档时,要确保在文档中说明所有成员
类或者静态方法是否线程安全,应该在文档中对他的线程安全级别进行说明
阅读由Javadoc工具生成的网页
通用编程
57.将局部变量的作用域最小化
要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明
使用时声明而非在代码块的开头进行声明!
几乎每一个局部变量的声明都应该包含一个初始化表达式,即xxx = xxx;而非 xxx;try-catch语句例外,有些情况需要在try块之前声明,块内初始化
循环中提供了特殊的机会来讲变量的作用域最小化。无论传统的for循环还是for-each形式的for循环,都允许声明循环变量loop variable ,它们的作用域被限定在正好的循环体及之前的初始化,测试,更新的范围内
若在循环终止之后不再需要循环变量的内容则for循环就优先于while循环
最后一种将局部变量的作用域最小化的方法是使方法小而集中
58.for-each循环优先于传统的for循环
for-each循环通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能
有三种情况无法使用for-each循环:
解构过滤——若需要遍历集合并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。使用java8中增加的Collection的removeIf方法,常常可以避免显式的遍历。
转换——若需要遍历列表或者数组,并取代它的部分或者全部元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
平行迭代——若需要并行地遍历啊多个集合,就需要显式的迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进
for-each不仅能遍历集合和数组,还能遍历实现Iterable接口的任何对象
59.了解和使用类库
通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验
从java7开始不再使用Random,现在选择随机数生成器时,大多使用ThreadLocalRandom,它会产生高质量的随机数,且速度非常快。对于Fork Join Pool和并行Stream则使用SplittableRandom
在每个重要的发行版本中,都会有许多新的特性被加入到类库中,所以与这些新特性保持同步是值得的
java9在InputStream中增加了transferTo方法
打印出命令行中指定的一条URL的内容
try(InputStream in = new URL(args[0]).openStream()) {
in.transferTo(System.out);
}
每个程序员都应该熟悉java.lang;java.util;java.io及其子包中的内容
Collections Framework集合框架和Stream类库应该成为每一个程序员的基本工具箱其一部分
Google开源Guava类库是优秀的第三方类库
60.如果需要精确地答案,请避免使用float和double
float和double执行二进制浮点运算
float和double类型尤其不适合用于货币计算
使用BigDecimal,int或者long进行货币运算
使用BigDecimal有两个缺点:与基本运算类型相比,不方便且速度很慢
61.基本类型优先于装箱基本类型
对装箱基本类型运用==操作符几乎总是错误的
若需要用比较器描述一个类型的自然顺序,只要调用Comparator.naturalOrder()即可
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱,此时null对象引用被自动拆箱,报空指针异常
当作为集合中的元素,键和值;在参数化类型和方法中;在进行反射的方法调用时必须使用装箱基本类型。
自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险
程序进行涉及装箱和拆箱基本类型的混合类型计算时,会进行拆箱,当程序进行拆箱时,会抛出空指针异常,最后,当程序装箱了基本类型值时,会导致较高的资源消耗和不必要的对象创建
62.如果其他类型更适合,则尽量避免使用字符串
字符串不适合代替其他的值类型,若获取的字符串信息是数值,就应该被转换为适当的数值类型,如int,float,BigInteger类型;若是是否问题的答案则转换为boolean类型。若存在适当的值类型,不管是基本类型还是对象引用,大多应该使用这种类型;
字符串不适合代替枚举类型,常量
字符串不适合代替聚合类型。
字符串不适合代替能力表capabilities
public class ThreadLocal {
private ThreadLocal() {}
public static void set(String key, Object value);
public static Object get(String key);
}
以上字符串实现的多线程授权访问可能存在冲突
public class ThreadLocal {
private ThreadLocal() {}
// Capability
public static class Key {
Key() {}
}
public static void key(Key key, Object value);
public static Object get(Key key);
}
改善
public final class ThreadLocal<T> {
public ThreadLocal() {}
public void set(T value);
public T get();
}
63.了解字符串连接的性能
为连接n个字符串而重复地使用字符串连接操作,需要n的平方级的时间
为了获得可以接受的性能,请用StringBuilder代替String
不要使用字符串连接操作符来合并多个字符串
64.通过接口引用对象
如果有合适的接口类型存在,那么对于参数,返回值,变量和域来说,就都应该使用接口类型进行声明
如果养成了用接口作为类型的习惯,程序将会更加灵活,如下
List<Integer> list = new ArrayList<>();
若没有合适的接口存在,完全可以用类而不是接口来引用对象,基类抽象类,或不可变的值类String等
如果没有适合的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象
65.接口优先于反射机制
核心反射机制core reflection facility,java.lang.reflect包,提供了访问任意类的能力。给定一个Class对象,可以获得Constructor,Method和Field实例,它们分别代表了该Class实例所表示的类的构造器,方法和域,这些对象提供了访问类的成员名称,域类型,方法签名等信息的能力
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,这种能力有以下缺陷
- 损失了编译时类型检查的优势,包括异常检查
- 执行反射访问所需要的代码非常笨拙和冗长
- 性能损失
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处
用反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例
若编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类
66.谨慎地使用本地方法
JNI Java Native Interface 允许Java应用程序调用本地方法,所谓本地方法是指用本地编程语言c,c++来编写的方法。
使用本地方法来提供性能的做法不值得提倡
67.谨慎地进行优化
不要为了性能而牺牲合理的结构。要努力编写好的程序而不是快的程序
要努力避免那些限制性能的设计决策
要考虑API设计决策的性能后果,在API中使用实现类型而不是接口,将无法得益于将来更快的实现
为获得好的性能而对API进行包装,这是一种非常不好的想法
在每次试图做优化之前和之后,要对性能进行测量
再多的底层优化也无法弥补算法的选择不当
68.遵守普遍接受的命名惯例
字面的命名惯例:
类型参数名称通常由单个字母组成。这个字母通常是以下五种类型之一:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。函数的返回类型通常是R。任何类型的序列可以是T,U,V或T1,T2,T3
标识符类型 | 示例 |
---|---|
包或者模块 | com.google.common.collect |
类或者接口 | Stream, FutureTask,HttpClient |
方法或者域 | remove, getCrc |
常量域 | MIN_VALUE |
局部变量 | i, denom, houstNum |
类型参数 | T,E,K,V,X,R,U,V,T1,T2 |
语法的命名惯例:
可被实例化的类包括枚举类型 用一个名词或者名词短语命名,如Thread,ChessPiece
不可实例化的工具类经常用复数名词命名:Collectors,Collections
接口的命名:Collection,Collector,或者用以able或ible结尾的形容词命名如 Runnable,Iterable,Accessible
执行某个动作的方法通常用动词或者动词短语append,drawImage
对于返回boolean值的方法,名称通常以is开头,后面跟名词或名词短语或形容词 isEmpty,isEnabled
转换对象类型的实例方法,返回不同类型的独立对象的方法,通常用toType命名。返回视图的方法一般用asType命名如asList
异常
69.只针对异常的情况才使用异常
基于异常的模式要比标准模式慢得多
异常应该只用于异常的情况下;它们永远不应该用于正常的控制流
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
70.对可恢复的情况使用受检异常,对编程错误使用运行时异常
java提供了三种可抛出接口throwable:受检异常checked exception,运行时异常run-time exception和错误error
受检异常CheckedException:必须要捕获处理;非受检异常:运行时异常,error,可以不捕获;
在决定使用受检异常或是未受检异常时,主要的原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常
用运行时异常来表明编程错误
你实现的所有未受检的抛出结构都应该是RuntimeException的子类
总之,对于可恢复的情况要抛出受检异常;对于程序错误,要抛出运行时异常;不确定是否可恢复,则抛出未受检异常
71.避免不必要地使用受检异常
抛出受检异常的方法不能直接在Stream中使用。Stream中只能捕获或者抛出非受检异常
消除受检异常最容易的方法是,返回所要的结果类型的一个optional,这个方法不抛出受检异常,而只是返回一个零长度的optional,这个方式的缺点在于,方法无法返回任何额外的信息,来说明执行失败的原因
把受检异常变成未受检异常的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean值,表明是否应该抛出异常。
即
try {
obj.action(args);
} catch() {
...// Handle exceptional condition
}
重构为:
if (obj.actionPermitted(args)) {
obj.action(args);
} else {
...// Handle exceptional condition
}
以上重构缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在actionPermitted和action两个调用的时间间隔中,对象的状态可能会改变
总而言之,在谨慎使用的前提下,受检异常可以提升程序的可读性;不能过度使用。若调用者无法恢复失败,就应该抛出非受检异常。若可以恢复,且想要迫使调用者处理异常的条件,首选应该返回一个optional值。当且仅当失败,这些无法提供足够的信息时,才应该抛出受检异常。
72.优先使用标准的异常
经常被重用的异常类型是IllegalArgumentException。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。另一个经常被重用的异常时IllegalStateException。如果因为接受对象的状态而使调用非法,通常就会抛出这个异常,如某个对象在初始化之前,调用者就企图使用这个对象
另一个值得了解的通用异常时ConcurrentModificationException。若检测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发的修改,就应该抛出这个异常。
最后一个值得注意的标准异常是UnsupportedOperationException。若对象不支持所请求的操作,就会抛出这个异常。如对于只支持追加操作的List实现,若有人试图从列表中删除元素就会抛出这个异常
public void remove(E e) {
throw UnsupportedOperationException("不支持删除");
}
不要直接重用Exception,RuntimeExceptiion,Throwable或者Error,对待这些类要像对待抽象类一样
常见可重用异常:
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 并发修改异常,禁止并发修改的情况下检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
子类化标准异常,注意异常是可序列化的
若没有可用的参数值,就抛出IllegalStateException,否则就抛出IllegalArgumentException
74.抛出与抽象对应的异常
更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法称为异常转译
try {
// 底层抛出异常,并按高层的异常捕获转译底层的抛出异常,即捕获Lower抛出Higher
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
一种特殊的异常转译形式称为异常链exception chaining,底层的异常(原因)被传到高层的异常,高层的异常提供访问方法Throwable的getCause来获得底层的异常:
try {
// 底层抛出异常,层层往高层传递
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
大多数标准的异常都支持链的构造器。若没有支持联的异常,可以利用Throwable的initCause方法设置原因
尽管异常转译与不加选择地从底层传递异常的做法相比有所改进,但是也不能滥用它
74.每个方法抛出的所有异常都要建立文档
**始终要单独地声明受检异常,**且利用javadoc的@throws标签,准确地记录下抛出每个异常的条件,永远不要声明一个公有方法直接throws Exception;main方法可以安全地声明抛出Exception,因为它只通过虚机调用
使用javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中,即不声明抛出运行时异常
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的
75.在细节消息中包含失败-捕获信息
为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和域的值
千万不要在细节消息中包含密码,密钥以及类似的信息
76.努力使失败保持原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性
失败原子性的设计:
- 设计一个不可变的对象,操作失败不会使已有对象改变状态
- 在可变对象上执行操作的方法,在执行操作之前检查参数的有效性。这使得对象的状态在修改前,先抛出适当的异常
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
三种实现失败原子性的方式:
- 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
- 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
- 编写一段回复代码,即回滚
77.不要忽略异常
空的catch块会使异常达不到应有的目的
如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4;
try {
numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
// Use default:minimal coloring is desirable,not required
}
并发
78.同步访问共享的可变数据
java语言规范保证读或者写一个变量是原子的atomic,除非这个变量的类型是long或者double,即读取一个非long或double类型的变量,可以保证返回值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此
虽然语言规范保证了线程在读取原子数据时,不会看到任意的数值,但它不能保证一个线程写入的值对于另一个线程将是可见的。
为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
千万不要使用Thread.stop方法,要则指一个线程妨碍另一个线程,建议让第一个线程轮询poll一个boolean域,这个域初始false,可通过第二个线程设置为true,以表示第一个线程将终止自己,由于boolean域的读和写操作都是原子的,程序员在访问这个域的时候不再需要使用同步
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
以上代码没有同步,线程永远循环
因为
while(!stopRequested)
i++;
被虚拟机优化(提升hoisting),结果活性失败liveness failure
if (!stopRequested)
while(true)
i++;
修正方式是同步访问stopRequested域
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested()) {
i++;
}
System.out.println(i);
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
以上写方法requestStop和读方法stopRequested都被同步了,除非读和写操作都被同步,否则无法保证同步起作用
注意while会被虚拟机提升,故而while的条件若在代码块中更新则直接使用变量,在代码块外更新则使用方法返回值的方式
注意以上代码被同步的方法中的动作是原子的,同步的开销会比较大故而用volatile修正如下
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
System.out.println(i);
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested true;;
}
}
volatile不指定互斥访问,但它保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值;实测效率低于互斥访问
注意使用volatile的时候,无法同步增量操作符++,因为++不是原子的,它相当于读取值,写回一个新值两步操作,此时使用volatile无法保证同步,导致安全性失败
此方法安全性失败
private static volatile int next = 0;
public static int generate() {
return next++;
}
修正
private static final AtomicLong next = new AtomicLong();
public static long generate() {
return next.getAndIncrement();
}
防止线程问题之一是不共享可变的数据,将可变数据限制在单个线程中
安全发布:约定共享一个数据对象,不对其进行任何修改,此时其他线程无需同步也可读取该对象,安全发布对象引用有多种方法:将它保存在静态域,作为类初始化的一部分;可以将它保存在volatile域,final域或者通过正常锁定访问的域中,或者放进并发的集合中
当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
79.避免过度同步
为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法
将外来方法的调用移出同步的代码块,可通过java类库提供的并发集合:CopyOnWriteArrayList非阻塞队列
此集合是ArrayList的变体,通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度较快。CopyOnWriteArrayList用于读多写少的场景
在同步区域之外被调用的外来方法被称作开放调用
通常来说,应该在同步区域内做尽可能少的工作
编写一个可变的类有两种选择:省略所有的同步,若想要并发使用,就允许客户端在必要的时候从外部同步;或者通过内部同步,使这个类变成线程安全的(82),如此获得明显比外部锁定整个对象更高的并发性。
java.util中的集合除了Vector和Hashtable外采用第一种选择
java.util.concurrent中的集合采用后一种选择
80.executor,task和stream优先于线程
ExecutorService exec = Excutors.newSingleThreadExecutor();
提交一个runnable的方法
exec.execute(runnable);
终止
exec.shutdown();
轻量负载的服务器使用Executors.newCachedThreadPool
大负载服务器使用Executors.newFixedThreadPool:提供固定线程数目的线程池,防止线程开辟太多,占用cpu资源
或者ThreadPoolExecutor类,最大限度控制它
并发的stream是在fork join池上编写的
81.并发工具优先于wait和notify
正确地使用wait和notify比较困难,就应该用更高级的并发工具代替
java.util.concurrent中的更高级的工具分成三类:Executor Framework,并发集合Concurrent Collection以及同步器Synchronizer
并发集合为标准的集合接口如List,Queue,Map提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢
有些并发集合接口通过依赖状态的修改操作进行了扩展,它们将几个基本操作合并到了单个原子操作中。它们通过缺省方法被加到了java8对应的集合接口中。
如Map的putIfAbsent(key, value)方法,当键没有映射时会替他插入一个映射,并返回与键关联的前一个值,若没有这样的值,则返回null。如此能容易实现线程安全的标准map
String.intern() java7以上
会先查询字符串常量池,若没有又会到堆中再去查询并存储堆的引用,然后返回
final String str = "bbb";
final String str2 = new String("bbb").intern();
System.out.println("str == str2 : "+(str == str2));
private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
public static String intern(String s) {
String previousValue = map.putIfAbsent(s, s);
return previousValue == null ? s : previousValue;
}
然而ConcurrentHashMap对获取操作即get进行了优化。因此,只有当get表明有必要的时候,才值得先调用get,再调用putIfAbsent:
public static String intern(String s){
String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
注意String.intern必须使用某种弱引用,避免随着时间的推移发生内存泄漏
应该优先使用ConcurrentHashMap而不是使用Collections.synchronizedMap
阻塞队列可用于工作队列,生产者-消费者队列,一个或者多个生产者线程添加工作项目,且工作项目可用时,一个或者多个消费者线程则从工作队列中取出队列并处理工作项目
同步器Synchronizer是使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch倒计时和Semaphore信号灯。不常用的是CyclicBarrier和Exchanger。功能最强大的同步器是Phaser
倒计数锁存器Countdown Latch是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountdownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数
对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis,因为nanoTime更准确,也更精确,它不受系统的实时时钟的调整所影响
始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法
82.线程安全性的文档化
因为在一个方法声明中出现synchronized修饰符,这是个实现细节,并不是导出的API的一部分
一个类可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全级别
- 不可变的 immutable
- 无条件的线程安全 unconditionally thread-safe
- 有条件的线程安全 conditionally thread-safe
- 非线程安全 not thread-safe
- 线程对立的 thread-hostile
lock域应该始终声明为final
83.慎用延迟初始化
延迟初始化lazy initialization是指延迟到需要域的值时才将它初始化的行为。懒加载
在大多数情况下,正常的初始化要优先于延迟初始化
若利用延迟优化来破坏初始化的循环,就要使用同步访问方法
private FieldType field;
private synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
}
若出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式
静态内部类
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
若出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式:两次检查域的值
private volatile FieldType field;
private FieldType getField() {
// result确保field只在已经被初始化的情况下读取一次,提升性能
FieldType result = field;
if (result == null) {
synchronized(this) {
if (field == null) {
field = result = computeFieldValue();
}
}
}
return result;
}
静态域使用静态内部类,实例域使用双重检查
不建议重复初始化则使用单重检查
84.不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的
若线程没有在做有意义的工作,就不应该运行
不要企图通过调用Thread.yield来修正该程序
Thread.yield没有可测试的语义
线程优先级是Java平台上最不可移植的特征了
序列化
85.其他方法优先于java序列化
对象图是通过在ObjectInputStream上调用readObject方法进行反序列的,该方法,可将类路径上几乎任何类型的对象都实例化,只要改类型实现了serializable接口。
避免序列化攻击的最佳方式是永远不要反序列化任何东西
在新编写的任何新系统中都没有理由再使用java序列化
使用json文本,Protocol Buffers二进制
永远不要反序列化不被信任的数据
java9新增对象反序列化过滤ObjectInputFilter
白名单优于黑名单
86.谨慎地实现Serializable接口
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了改变这个类的实现的灵活性
实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性,即反序列是一个隐藏的构造器,缺少约束关系
实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也会增加
实现Serializable接口并不是一个很轻松就可以做出的决定
为了继承而设计的类,应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少继承Serializable接口
内部类不应该实现Serializable接口
count不为null sum可以为null