Java8之前创建函数对象的方式是使用匿名类, 比如下面的代码:
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("hello");
words.add("world");
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
}
匿名类满足了传统的面向对象的设计模式对函数对象的需求, 最有名的是策略模式. Comparator接口表示一种排序的抽象策略, 匿名类则是具体策略, 但使用匿名类十分烦琐. Java8后, 形成了带有单个抽象方法的接口是特殊的, 值得特殊对待的观念, 这些接口成为函数接口.Java允许利用Lambda表达式创建这些接口的实例:
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
除非无法推导出Lambda的参数的类型, 否则删除所有的Lambda参数的类型.还有更简便的写法:
Collections.sort(words, Comparator.comparingInt(String::length));
words.sort(Comparator.comparingInt(String::length));
原34条的Operation类可改为:
/
* 四大运算: 特定于常量的方法实现
*
* @author lzlg
* 2021/5/1 18:30
*/
enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y),
;
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return this.symbol;
}
public double apply(double x, double y) {
return this.op.applyAsDouble(x, y);
}
}
Lambda没有名称和文档, 如果一个计算本身不是自描述的, 或者超出了几行, 就不要放在一个Lambda中, 一行是最理想的, 三行是合理的最大极限.
Lambda仅限于函数接口, 如果想创建抽象类的实例, 可使用匿名类完成. 匿名类为带有多个抽象方法的接口创建实例. Lambda无法获得对自身的引用, 在Lambda中this指外围实例, 而在匿名类中this指匿名类实例.
无法可靠的实现Lambda和匿名类的序列化和反序列化, 尽可能不要进行序列化一个Lambda或匿名类.
Java提供了比Lambda更简洁的函数对象的方法: 方法引用.
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
String key = "123";
map.merge(key, 1, (count, incr) -> count + incr);
map.merge(key, 1, (count, incr) -> count + incr);
System.out.println(map);
}
map的merge方法, 如果key没有对应的值, 则把第二个参数(上面例子是1)当作value进行插入, 如果有对应的值, 则在原value基础上进行增加(增加的值为第二个参数). 可用Integer的sum静态方法:
map.merge(key, 1, Integer::sum);
使用方法引用可得到更简短清晰的代码, 如果lambda太长, 过于复杂, 从lambda提前代码, 放到一个新方法中, 并用该方法的一个引用代替lambda.
方法引用的5中用法:
引用类型 | 范例 | Lambda等式 |
---|---|---|
静态 | Integer::parseInt | str -> Integer.parseInt(str) |
有限制 | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
无限制 | String::toLowerCase | str -> str.toLowerCase() |
类构造器 | TreeMap<K, V>::new | () -> new TreeMap<K, V> |
数组构造器 | int[]::new | len -> new int[len] |
只要方法引用更加简洁清晰就用, 如果不简洁, 就坚持使用lambda.
只要标准的函数接口能够满足需求, 通常应该优先考虑, 而不是专门再构建一个新的函数接口.
6个基础的函数接口:
接口 | 函数签名 | 范例 |
---|---|---|
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 t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
现有的大多数标准函数接口都只支持基本类型, 千万不要用带包装类型的基础函数接口来代替基本函数接口, 会导致致命的性能问题.
当没有任何标准的函数接口能够满足需求时, 应该编写自己的函数接口, 有以下特征的:
如果是自己编写的函数接口, 则必须添加@FunctionInterface注解.
一个Stream Pipeline中包含一个源Stream, 接着是0个或多个中间操作和一个终止操作.Stream pipeline通常是lazy的, 直到调用终止操作时才会开始计算, 对于完成终止操作不需要的的数据元素, 将永远不会被计算. 没有终止操作的Stream pipeline是一个静默的无操作指令, 因此千万不能忘记终止操作.Stream API是流式的, 所有包含pipeline的调用可以链接成一个表达式, 默认情况下Stream pipeline是按照顺序运行的.
下面的程序的作用是从词典文件中读取单词, 并打印出单词长度符合用户指定的最低值的所有换位词(包含相同的字母,但字母的顺序不同的两个词):
/
* @author lzlg
* 2022/11/17 16:33
*/
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);
}
for (Set<String> group : groups.values()) {
if (group.size() >= minGroupSize) {
System.out.println(group.size() + ": " + group);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String alphabetize(String s) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
return new String(chars);
}
}
如果上述例子全部使用Stream. 则为:
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) {
e.printStackTrace();
}
}
上述代码虽然简短, 但是难于读懂, 滥用Stream会使程序代码更难以读懂和维护. 好在有以下方法:
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)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
private static String alphabetize(String s) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
return new String(chars);
}
在Stream pipeline中使用helper方法, 对于可读性而言, 比在迭代化代码中使用更为重要.最后避免利用Stream处理char值(会变为int值).
下列工作只能通过代码块, 不能通过函数对象来完成:
Stream可使以下工作完成更好:
利用Stream很难完成同时从一个pipeline的多个阶段去访问相应的元素, 一旦将一个值映射到某个其他值, 原来的值就丢失了.
// 生成梅森素数
public class BigPrime {
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(BigInteger.ONE))
.filter(p -> p.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
private static final BigInteger TWO = BigInteger.valueOf(2);
private static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
迭代和Stream的取舍:
// 使用迭代,代码清晰易读
public static void main(String[] args) {
List<Card> result = new ArrayList<>();
for (Suit suit : Suit.values()) {
for (Rank rank : Rank.values()) {
result.add(new Card(suit, rank));
}
}
System.out.println(result);
}
private enum Suit {
ONE, TWO, THREE;
}
private enum Rank {
RED, WHITE, BLACK;
}
private static class Card {
private final Suit suit;
private final Rank rank;
public Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
}
// 完全使用Stream, 会使不熟悉Stream的程序员读不懂
public static void main(String[] args) {
List<Card> result = Stream.of(Suit.values())
.flatMap(suit -> Stream.of(Rank.values()).map(rank -> new Card(suit, rank)))
.collect(Collectors.toList());
System.out.println(result);
}
Stream范型最重要的部分是把计算构造成一系列变型, 每一级结果都尽可能靠近上一级结果的纯函数(结果只取决于输入的函数, 不依赖任何可变的状态, 也不更新任何状态).为了做到这一点, 传入Stream操作的任何函数对象, 无论是中间操作或终止操作, 都应该是无副作用的.
public static void main(String[] args) {
Path path = Paths.get(args[0]);
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = Files.lines(path)) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
} catch (Exception e) {
e.printStackTrace();
}
}
上述代码不是Stream代码, 是伪装成Stream代码的迭代式代码, 没有享受到Stream API带来的优势.forEach操作应该只用于报告Stream计算的结果,而不是执行计算.
freq = words.collect(Collectors.groupingBy(String::toLowerCase, Collectors.counting()));
将Stream元素集中到一个真正的Collection的收集器API: toList(), toSet(), toCollection(collectionFactory), 分别返回一个列表, 一个集合和程序员指定的集合类型.
// 获取赔率表中排名前10的单词列表
List<String> topTen = freq.keySet().stream()
.sorted(Comparator.comparing(freq::get).reversed())
.limit(10)
.collect(Collectors.toList());
最简单的映射收集器是toMap(keyMapper, valueMapper), 有两个函数, 一个是将Stream元素映射到键, 另一个是将它映射到值.
三个参数的toMap形式:
四个参数的toMap形式: 第四个参数是一个映射工厂, 在使用时要指定特殊的映射实现, 如EnumMap.
toMap对应的同步Map方法是toConcurrentMap, 和toMap上述的方法一一对应,返回ConcurrentHashMap.
Collectors API提供了groupingBy方法, 返回收集器以生成映射, 根据分类函数将元素分门别类.最简单的是只有一个分类器, 并返回一个映射, 映射值为每个类别中所有元素的列表.
如果要让groupingBy返回一个收集器, 用它生成一个值而不是列表的映射, 除了分类器外, 还可指定一个下游收集器, 下游收集器从包含某个类别中的所有元素的Stream中生成一个值.比如toSet(), toCollection(collectionFactory).
带两个参数的groupingBy的简单用法, 传入counting()作为下游收集器, 这样会生成一个映射, 将每个类别与该类别中的元素数量关联起来.
groupingBy除了指定下游收集器外, 还可指定一个映射工厂, 还可控制所包围的映射, 以及包围的集合.
groupByConcurrent方法提供了groupingBy所有三种重载的变体, 变体可有效并发的运行, 生成ConcurrentHashMap实例.
minBy和maxBy有一个比较器, 并返回由比较器确定的Stream中最少或最多的元素.
joining方法只在CharSequence实例的Stream中操作, 以参数的形式返回一个简单地合并元素的收集器. 其中一个参数形式带有一个名为delimiter(分界符)的CharSequence的参数, 返回一个链接Stream元素并在相邻元素之间插入分隔符的收集器.三种参数形式, 除了分隔符之外, 还有一个前缀和一个后缀.
如果在编写一个返回对象序列的方法时, 就知道它只在Stream pipeline中使用, 可放心返回Stream.相反, 当返回序列的方法只在迭代中使用时, 则应该返回Iterable.如果是用公共的API返回序列,则应该为那些想要编写Stream pipeline以及也想要编写foreach语句的用户分别提供.
Collection接口是Iterable的一个子类型, 它有一个stream方法, 因此提供了迭代和stream访问.对于公共的, 返回序列的方法, Collection或适当的子类型通常是最佳的返回类型.但千万别在内存中保存巨大的序列,将它作为集合返回即可.
// 返回一个集合的所有子集, 包括空集和自身
public class PowerSet {
public static void main(String[] args) {
Set<String> s = new HashSet<>();
s.add("a");
s.add("b");
s.add("c");
Collection<Set<String>> powerSet = of(s);
for (Set<String> set : powerSet) {
System.out.println(set);
}
}
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30) {
throw new IllegalArgumentException("Set too big " + s);
}
return new AbstractList<Set<E>>() {
@Override
public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i = 0; index != 0; i++, index >>= 1) {
if ((index & 1) == 1) {
result.add(src.get(i));
}
}
return result;
}
@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set) o);
}
@Override
public int size() {
return 1 << src.size();
}
};
}
}
如果输入值集合中超过30个元素, Power.of会抛出异常, 这是用Collection而不是用Stream或Iterable作为返回类型的缺点: 有个返回int类型的size方法, 限制了返回的序列长度为Integer的最大值.
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(
Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes)
);
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Stream<List<String>> stream = of(list);
stream.forEach(System.out::println);
}
}
上述子列表的实现本质上和明显的嵌套for循环类似:
for (int start = 0; start < list.size(); start++) {
for (int end = start + 1; end <= list.size(); end++) {
System.out.println(list.subList(start, end));
}
}
上述for循环, 也可翻译成一个Stream,和for循环一样都没有空列表:
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start ->
IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))
.flatMap(x -> x);
}
子列表的Stream实现都很好, 但这两者都需要用户在任何更适合迭代的地方采用Stream-To-Iterable适配器或者用Stream, Stream-To-Iterable不但打乱了客户端代码, 循环速度还降低了.
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
在Stream上通过并行获得的性能, 最好是通过ArrayList, HashMap, HashSet和ConcurrentHashMap实例, 数组, int范围和long范围等.这些数据结构的共性是, 都可以被精确轻松地分成任意大小的子范围.Stream类库用来执行这个任务的抽象是分割迭代器, 是由Stream和Iterable中的spliterator方法返回的.
这些数据结构的另一个重要特性是, 在进行顺序处理时, 提供了优异的引用局部性: 序列化的元素引用以前保存在内存中.被那些引用访问到的对象在内存中可能不是一个紧挨着一个, 这降低了引用局部性.事实证明, 引用局部性对于进行批处理来说至关重要, 没有它, 线程就会出现闲置, 需要等待数据从内存转移到处理器的缓存.
Stream pipeline的终止操作本质上影响了并发执行的效率.如果大量的工作在终止操作中完成, 而不是全部工作在pipeline中完成, 并且这个操作是固有的顺序, 那么并行pipeline的效率就会受到限制.并行的最佳终止操作是做剑法,用一个Stream的reduce方法, 将所有从pipeline产生的元素合并在一起, 或预先打包像min, max, count和sum这类方法. 骤死式操作, 如anyMatch, allMatch和noneMatch也都可以并行.由Stream的collect方法执行的操作. 都是可变的减法, 不是并行的最好选择, 因为合并集合的成本非常高.
并行Stream不仅可能降低性能, 包括活性失败, 还可能导致结果出错, 以及难以预计的行为(如安全性失败).安全性失败可能使用了自己编写的其他函数对象, 但没有遵守它们的规范.
并行Stream Pipeline有效的例子:
private static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
private static long piParallel(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
如果要并行一个随机数的Stream, 应该从SplittableRandom实例开始, 而不是从ThreadLocalRandom(或实际上已过时的Random).SplittableRandom正是专门为此设计的, 还有线性提速的可能.ThreadLocalRandom则只用于单线程, 它将自身当作一个并行的Stream源运用到函数中, 但是没有SplittableRandom那么快.
大多数方法和构造器对于传递给它们的参数值都会有某些限制, 应该在文档中清楚地指明这些限制, 并且在方法体地开头处检查参数, 以强制施加这些限制.
没有检查参数的有效性, 可能导致违背失败原子性.
在Java7中增加的Objects.requireNonNull方法比较灵活且方便, 因此不再手工进行null检查.
非公有的方法通常应该使用断言来检查它们的参数.
有些参数方法本身没有用到, 却被保存起来供以后使用, 检验这类参数的有效性尤为重要.构造器的参数也是如此.
规则的例外: 比如Collections.sort(List)方法, 有效性检查已隐含在计算过程中.
假设类的客户端会尽其所能来破坏这个类的约束条件, 因此必须保护性地设计程序.
public class TestPeriod {
private final Date start;
private final Date end;
public TestPeriod(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 start;
}
public Date getEnd() {
return end;
}
}
上面的类, 乍看之下是不可变得, 但Date类本身是可变得, 很容易违反start(开始日期)大于end(结束日期)得约束条件.
Java8之后修正这个问题使用Instant(或 LocalDateTime或ZonedDateTime)代替Date. 因为都是不可变类.
为了保护TestPeriod实例的内部信息受到攻击, 对于构造器的每个可变参数进行保护性拷贝是必要的.
public TestPeriod(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝后的对象,而不是针对原始对象.这样做可避免从检查参数开始, 直到拷贝参数之间的时间段里另一个线程改变类的参数,成为Time-Of-Check/Time-Of-Use或TOCTOU攻击.
没有用Date的clone方法来进行保护性拷贝, 因为Date是非final的, 可能会返回专门出于恶意的目的而设计的不可信的子类的实例.对于参数类型可以被不可信任方子类化的参数, 不要使用clone方法进行保护性拷贝.
但改变TestPeriod实例还是有可能的, 因为它的访问方法(getter)提供了对可变成员的访问能力.为了防御这种攻击, 修改getter方法返回可变内部域的保护性拷贝:
public Date getStart() {
return new Date(this.start.getTime());
}
public Date getEnd() {
return new Date(this.end.getTime());
}
谨慎地选择方法的名称, 方法签名应该始终遵循标准的命名习惯,易于理解,风格一致,大众认可的.
不要过于追求提供便利的方法.
避免过长的参数列表, 三个技巧:
对于参数类型, 优先使用接口而不是类.如果使用的是类而不是接口, 则限制了客户端只能传入特定的实例, 如果碰巧输入的数据是以其他的形式存在, 就会导致不必要的, 可能非常昂贵的拷贝操作.
对于boolean参数, 要优先使用两个元素的枚举类型.使代码更易阅读和编写, 也方便以后添加其他的选项.
下面的程序:
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> l) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
// 原希望打印出 Set List Unknown Collection
// 但是却打印出三个 Unknown Collection
for (Collection<?> c : collections) {
System.out.println(classify(c));
}
}
}
出现上述问题, 是因为classify被重载了, 要调用哪个重载方法是在编译时做出决定的.对于for循环中的三次迭代, 参数的编译时类型都是相同的: Collection<?>.
对于重载方法的选择是静态的(编译时), 对于被覆盖的方法的选择则是动态的(运行时).
public class TestOverriding {
public static void main(String[] args) {
List<Wine> wineList = Arrays.asList(new Wine(),
new SparklingWine(),
new Champagne());
for (Wine wine : wineList) {
System.out.println(wine.name());
}
}
private static class Wine {
String name() {
return "wine";
}
}
private static class SparklingWine extends Wine {
@Override
String name() {
return "sparkling wine";
}
}
private static class Champagne extends Wine {
@Override
String name() {
return "champagne";
}
}
}
CollectionClassifier的最佳修改方案是, 用单个方法来替换这个三个重载的classify方法:
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" :
c instanceof List ? "List" : "Unknown Collection";
}
安全保守的策略是, 永远不要导出两个具有相同参数数目的重载方法, 始终给方法起不同的名称, 而不是使用重载机制.
对于构造器, 没有选择使用不同名称的机会, 一个类的多个构造器总是重载的.如果对于任何一组给定的实际参数将应用于哪个重载方法上始终非常清楚, 那么导出多个具有相同参数数目的重载方法就不可能使程序员感到混肴.
对于每一对重载方法,至少有一个对应的参数在两个重载方法中具有根本不同的类型, 就属于上述情况.
但自动装箱和泛型自从成了Java语言的组成部分后, 谨慎重载更加重要:
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
// 原期待都打印出 [-3, -2, -1]
// 但却打印出: set: [-3, -2, -1] list: [-2, 0, 2]
System.out.println("set: " + set);
System.out.println("list: " + list);
}
}
上述问题: set.remove(i)调用选择重载方法remove(E)E为Integer类型, 将i从int自动装箱到Integer中.
list.remove(i)调用选择重载方法remove(int i), 故得到的是[-2, 0, 2], 为了得到 [-3, -2, -1] 需将list.remove的参数转换为Integer, 迫使选择正确的重载方法.List
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i);
// 或者
list.remove(Integer.valueOf(i));
}
System.out.println("set: " + set);
System.out.println("list: " + list);
}
}
Java8中增加了lambda和方法引用后, 进一步增加了重载造成的混肴:
public class TestMethodRef {
public static void main(String[] args) {
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
// 注意: 下面的不会编译
exec.submit(System.out::println);
}
}
submit方法有一个带有Callable
当两个重载方法在同样的参数上被调用时, 它们执行的是相同的功能, 重载就不会带来危害, 只要返回的相同的结果就行. 比如String的contentEquals(StringBuffer) 和contentEquals(CharSequence).确定这种行为标准的做法是, 让更具体化的重载方法把调用转发给更一般化的重载方法:
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
可变参数机制首先会创建一个数组, 数组的大小在调用位置所传递的参数数量, 然后将参数值传到数组中, 最后将数组传递给方法.
假设要编写一个函数来计算多个参数的最小值, 如果客户端没有传递参数, 则需在运行时检查数组长度:
private 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;
}
但有几个问题: 一是客户端没有传递参数, 只能在运行时而不是编译时发生失败, 二是代码不美观,必须在args中包含显式的有效性检查,无法使用for-each循环.可修改为:
private static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs) {
if (arg < min) {
min = arg;
}
}
return min;
}
在重视性能的情况下, 使用可变参数机制要特别小心, 每次调用可变参数方法都会导致一次数组的分配和初始化,如果无法承受这一成本, 但需要可变参数的灵活性.假设确定对某个方法95%的调用会有3个或更少的参数, 就声明该方法的5个重载, 每个重载方法带有0至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:
public void foo(){}
public void foo(int a1){}
public void foo(int a1, int a2){}
public void foo(int a1, int a2, int a3){}
public void foo(int a1, int a2, int a3, int... rest){}
对于一个返回null而不是零长度数组或集合的方法, 几乎每次用到该方法都需要处理null返回值的情况, 这样做很容易出错, 也没有任何的性能优势:
private List<Cheese> cheesesInStock = Arrays.asList(new Cheese(), new Cheese());
// 不再返回null,返回Collections.emptyList()空列表
public List<Cheese> getCheeseList() {
return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}
// 不再返回null,返回零长度的数组
public Cheese[] getCheeseArray() {
return cheesesInStock.toArray(new Cheese[0]);
}
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
// 可重复返回零长度的数组,因为不可变
public Cheese[] getCheeseArray2() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
private static class Cheese {
}
Java8之前,要编写一个在特定环境下无法返回任何值的方法时, 有两种方法: 要么抛出异常, 要么返回null.抛出异常的开销很高, 返回null需要客户端进行处理, 两种方式都不完美.
Java8后,可返回Optional
// 不使用Optional只能抛出异常
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("Empty Collection");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
// 使用optional
public static <E extends Comparable<E>> Optional<E> maxOptional(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);
}
将null传入Optional.of(value)是一个编程错误, 如果这么做会抛出NullPointerException. Optional.ofNullable(value)方法接收可能为null的值,当传入null时返回一个空的Optional.永远不要通过返回Optional的方法返回null, 因为违背了optional的本意.
Optional可以指定一个缺省值: orElse(). 也可以抛出任何适当的异常: orElseThrow().
如果从空的optional调用get方法, 则会抛出NoSuchElementException.
Optional还提供了isPresent()方法, 可以当作一个安全阀, 当optional包含一个值时, 它返回true, 当optional为空时返回false.
容器类型包括集合, 映射, Stream, 数组和optional, 都不应该被包装在optional中.
声明一个方法来返回Optional
永远不应该返回基本包装类型的Optional,因比返回一个基本类型的开销更高,因为optional有两级包装.但小型的基本类型(Boolean Byte Character Short Float)除外.
几乎永远都不适合用optional作为键, 值或者集合或数组中的元素.
如果要想使一个API真正可用, 必须为其编写文档.
为了正确地编写API文档, 必须在每个被导出的类, 接口, 构造器, 方法和域声明之前增加一个文档注释.
方法的文档注释应该简洁地描述出它和客户端之间的约定.这个约定应该说明方法做了什么, 文档注释应该列举出这个方法的所有前提条件和后置条件, 还应该在文档中描述它的副作用.方法的文档注释应该让每个参数都有一个@param标签, 以及一个@return标签(除非是void), 方法抛出的异常(无论check还是unchecked)都应有一个@throw标签.
Javadoc工具会把文档注释翻译成Html. {@code}标签作用: 使该代码片段以code font(代码字体)呈现, 并限制html标记和嵌套的Javadoc标签在代码片段中进行处理.多行的代码示例前使用<pre></pre>
进行包裹.
在专门为了继承设计类时, 必须在文档中注释它的自用模式.应利用java8增加的@implSpec标签注释.
为了产生包含Html元字符的文档(即不进行转义), 比如 < > & 需使用{@literal}标签包围起来.
文档注释在源代码和产生的文档中都应该是易于阅读的.每个文档注释的第一句话成了该注释所在元素的概要描述.对于构造器和方法, 概要描述应该是个完整的动词短信, 描述方法所执行的动作. 使用第三人称时态比使用第二人称时态更加确切.对于类, 接口和域, 概要描述应该是一个名称短语.
当为泛型或者方法编写文档时, 确保要在文档中说明所有的类型参数.
当为枚举类型编写文档时, 要确保在文档中说明常量, 以及类型, 还有任何公有的方法.
为注解类型编写文档时, 要确保在文档中说明所有成员,以及类型本身.
类或者静态方法是否线程安全, 应该在文档中对它的线程安全级别进行说明.如果类是可序列化的, 则应该在文档中说明它的序列号形式.
要使局部变量的作用域最小化, 最有力的方法就是在第一次要使用它的地方进行声明.
几乎每一个局部变量的声明都应该包含一个初始化表达式, 例外: try-catch.
for循环优先于while循环,因声明的循环的变量, 作用域被限定在正好需要的范围之内.更简短,可读性强.
最后一种将局部变量的作用域最小化的方法是使方法小而集中.
for-each循环完全隐藏迭代器或者索引变量, 避免出现混乱和出错的可能.且不会有性能损失.
有三种情况无法使用for-each循环:
for-each循环不仅能遍历集合和数组, 还能遍历实现Iterable接口地任何对象.
通过使用标准库类, 可以充分利用这些编写标准类库的专家的知识, 以及在你之前的其他人的使用经验.
现在选择随机数生成器, 大多使用ThreadLocalRandom.对于Fork Join Pool和并行Stream, 则使用SplittableRandom.
使用标准类库, 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案, 应把时间花在应用程序上.
使用标准类库, 它们会随着时间的推移而增加新的功能.
使用标准类库, 可以使自己的代码融入主流, 代码更易读, 更易维护, 更容易被大多数的开发人员重用.
每个程序员都应该熟悉java.lang, java.util, java.io及其子包中的内容.
float和double类型没有提供完全精确的结果, 不应该被用于需要精确结果的场合.
float和double类型尤其不适合用于货币计算.应使用BigDecimal, int或者龙进行货币计算.
选用int或long取决于所涉及的数值的大小,(可以使用以分为单位进行计算). 如果数值范围没有超过9位十进制数字, 就可以使用int; 如果不超过18位数字, 可以使用long;如果数值超过18位数字, 就必须使用BigDecimal.
基本类型和装箱基本类型之前的三个主要区别:
public class TestBoxedBaseType {
public static void main(String[] args) {
Comparator<Integer> naturalOrder = (i, j) -> i < j ? -1 : (i == j ? 0 : 1);
// 原以为会打印0, 实际打印出1
System.out.println(naturalOrder.compare(new Integer(20), new Integer(20)));
}
}
其中i < j导致自动拆箱, 故为false, 但下面的i == j, 是在两个对象引用上执行同一性比较, 因为比较的是两个新创建的Integer对象, 故比较操作会返回false, 打印1.
对装箱基本类型使用==操作符几乎总是错误的.
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed;
return i < j ? -1 : (i == j ? 0 : 1);
};
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱, 如果null对象引用被自动拆箱, 就会抛出一个NullPointerException异常.
应该使用装箱基本类型的情况:
字符串不适合代替其他的值类型: 如果存在适当的值类型, 应该使用这种类型;如果不存在适当的值类型, 就应该编写一个类型.
字符串不适合代替枚举类型.
字符串不适合代替聚合类型.更好的做法是简单编写一个私有的静态成员类来描述数据集.
字符串不适合代替能力表.
设计一个提供线程局部变量的机制, 这个机制提供的变量在每个线程中都有自己的值,有些人使用这样的设计方案: 利用客户提供的字符串键对每个线程局部变量的内容进行授权访问:
private static class ThreadLocal {
private ThreadLocal() {
}
public static void set(String key, Object value) {
}
public static Object get(String key) {
return null;
}
}
这种方案的问题在于, 这些字符串键代表了一个共享的全局命名空间. 要使这种方案可行, 客户端提供的字符串键必须是唯一的, 如果使用相同的名称(key), 就共享了ThreadLocal中的Object(value), 会导致两个客户端都失败, 且安全性很差.恶意的客户端可能有意地使用与另一个客户端相同的键, 以便非法访问其他客户端的数据.
// 用一个不可伪造的key代替字符串类型的key
private static class ThreadLocal {
private ThreadLocal() {
}
private static class Key {
private Key() {
}
}
public static Key getKey() {
return new Key();
}
public static void set(Key key, Object value) {
}
public static Object get(Key key) {
return null;
}
}
// 也可不使用静态方法,可以被代之以键Key中的实例方法, 这样这个键不再是键而是线程局部变量
private static class ThreadLocal {
public ThreadLocal() {
}
public void set(Object value) {
}
public Object get() {
return null;
}
}
// 上面的代码不是类型安全的, 需将ThreadLocal泛型化
private static class ThreadLocal<T> {
public ThreadLocal() {
}
public void set(T value) {
}
public T get() {
return null;
}
}
字符串连接操作符+是把多个字符串合并为一个字符串的便利途径.要想产生单独一行的输出, 或者构造一个字符串来表示一个较小的, 大小固定的对象, 使用连接操作符是非常适合的.但它不适合运用在大规模的场景中.为连接n个字符串而重复地使用字符串连接操作符, 需要n的平方级的时间.这是因为字符串不可变而导致的不幸结果.当两个字符串被连接在一起时, 它们的内容都要被拷贝.
为了获得可以接受的性能, 请使用StringBuilder代替String.
如果有合适的接口类型存在, 那么对于参数, 返回值, 变量和域来说,都应该使用接口类型进行声明.
用接口作为类型, 程序将会更加灵活, 切换实现时, 只需改变构造器中类的名称, 周围的所有代码都可继续工作.
如果原来的实现提供了某种特殊的功能, 而这种功能并不是这个接口的通用约定所要求的, 且周围的代码又依赖这种功能, 那么新的实现也要提供同样的功能.
如果没有合适的接口存在, 完全可以用类而不是接口来引用对象.比如String和BigInteger
如果对象属于一个框架, 而框架的基本类型是类, 不是接口, 应该用相关的基础类(一般为抽象类)来引用这个对象,而不是它的实现类.
如果类实现了接口, 但它也提供了接口中不存在的额外方法, 如果程序依赖这些额外的方法, 这种类就应该只被用来引用它的实例, 永远也不应该用作参数类型.
反射机制允许一个类使用另一个类, 即使当前者编译的时候后者还不存在, 但这种能力要付出代价:
如果只是以非常有限的形式使用反射机制,虽然付出少许代价,但是可以获得很多好处.许多程序必须用到的类在编译时是不可用的, 但是在编译时存在适当的接口或者超类, 通过它们可引用这个类.如果是这种情况,就可以用反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例:
下面的程序创建了一个Set<String>
实例,它的类是由第一个命令行参数指定的, 程序把其余的命令行参数插入到这个集合中,然后打印该集合:
public static void main(String[] args) {
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>) Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameter less constructor");
}
Set<String> s = null;
try {
s = cons.newInstance();
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (IllegalAccessException e) {
fatalError("Constructor not accessable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
}
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
本地方法是指用本地编程语言来编写的方法.
使用本地方法来提高性能的做法不值得提倡,JVM实现变得越来越快,对于大多数任务,现在用Java就可以获得与之相当的性能.
本地语言不是安全的,本地语言是和平台相关的,不再是可移植的程序,本地方法难以调试,本地方法还可能降低性能,因为回收垃圾器不是自动的,甚至无法追踪本机内存使用情况.进入和退出本地方法, 需要相关的开销.需要胶合代码的本地方法编写起来单调乏味,难以阅读.
不要为了性能而牺牲合理地结构,要努力编写好的程序而不是快的程序,如果好的程序不够快,它的结构将使它可以得到优化.好的程序体现了信息隐藏的原则.
要努力避免那些限制性能的设计决策, 要考虑API设计决策的性能后果.
一旦精心设计了程序, 并且产生了一个清晰,简明,结构良好的实现,那么就到了考虑性能优化的时候.在每次试图做优化之前和之后,要对性能进行测量.
不要费力去编写快速的程序,应该努力编写好的程序,速度自然会随之而来.
命名惯例分为两大类: 字面的和语法的.
字面命名惯例:
语法命名惯例:
公认做法: 如果长期养成的习惯用法与此不同,请不要盲目遵从这些命名惯例.
基于异常的循环: (永远不要这样做)
int i = 0;
try {
while (true) {
Integer.valueOf(args[i++]);
}
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
正常的模式:
for (String arg : args) {
Integer.valueOf(arg);
}
基于异常的模式不仅模糊了代码的意图,降低了性能,还不能保证正常的工作,如果中间出现了相同的异常,则不能精确定位到异常的原因.
异常应该只用于异常的情况,永远不应该用于正常的控制流.
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常.
如果类有状态相关的方法(比如迭代器的next方法), 即只有在特定的不可预知的条件下才可被调用的方法, 这个类往往应该有个单独的状态测试方法(比如迭代器的hasNext方法),即指示是否可以调用这个状态相关的方法.
如果状态相关的方法无法执行想要的计算,就让他返回一个零长度的optional值, 或返回一个可识别的值,比如null值.
如果对象将在缺少外部同步的情况下被并发访问, 或者可被外界改变状态, 就必须使用optional返回值或可识别的返回值,因为在调用状态测试方法和调用对应的状态相关方法的时间间隔中, 对象的状态有可能发生变化.
如果单独的状态测试方法必须重复状态相关方法的工作,从性能的角度考虑,就应该使用可识别的返回值.如果所有其他方面都是等同的, 那么状态测试方法略优于可被识别的返回值,它提供了稍微更好的可读性,对于使用不当的情形可能更加易于检测和更正: 如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个bug变得很明显;如果忘了去检查可识别的返回值,这个bug就很难发现.optional返回值不会有这方面的问题.
使用受检异常或未受检异常的原则: 如果期望调用者能够适当地恢复,对于这种情况应使用受检异常.对使用API的用户潜在指示: 与异常相关联的条件是调用这个方法的一种可能结果.强制API用户从这个异常条件中恢复.
用运行时异常来表明编程错误.大多数运行时异常都表示前提违例(API的用户没有遵守API规范约定).
错误往往被JVM保留下来使用不要再实现任何新的Error子类.你实现的所有未受检的抛出结果都应该是RuntimeException的子类.
异常是个对象,可以在它上面定义任意的方法, 这些方法的用途是为捕获异常的代码而提供额外的信息,特别是关于引发这个异常条件的信息.
受检异常强迫程序员处理异常的条件,大大增强了可靠性.
如果正确地使用API并不能阻止这种异常条件地产生,并且一旦产生异常,使用API的程序员可立即采取有用的动作,这种负担就被认为是正当的.除非这两个条件都成立,否则更适合于使用未受检异常.
消除受检异常最容易的方法是返回所要的结果类型的一个optional,这个方法不抛出受检异常,只是返回一个零长度的optional.这种做法的缺点是无法返回任何额外的信息.
把受检异常变成未受检异常的一种方法: 把抛出一个异常的方法分成两个方法, 其中第一个方法返回一个boolean值,表明是否应该抛出异常.
Java平台类库提供了一组基本的未受检异常,满足了大多数API的异常抛出需求.
使用标准异常的好处: 1.使API更易于学习和使用, 2.可读性更好, 3.异常类越少, 内存占用越小.
常用的异常使用:
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
不要直接重用Exception, RuntimeException, Throwable或Error.
如果没有可用的参数值,就抛出IllegalStateException否则抛出IllegalArgumentException.
异常转译: 更高层的实现应该捕获底层的异常, 同时抛出可用按照高层抽象进行解释的异常.
异常链: 如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适.
try {
// 使用低层抽象代码
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
高层异常的构造器将原因传到支持链的超级构造器:
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
但异常转译不能滥用, 最好的做法是在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常.
如果无法阻止低层的异常,其次的做法是让更高层来悄悄处理这些异常,从而将高层方法的调用者与低层的问题隔离开来,可使用某种适当的记录机制将异常记录下来.
描述一个方法所抛出的异常, 是正确使用这个方法时所需文档的重要组成部分.
始终要单独地声明受检异常,且使用@throws标签, 准确地记录下抛出每个异常地条件.永远不要声明一个公有方法直接throws Exception或者throws Throwable.这样的声明不仅没有提供关于异常的任何指导信息,而且掩盖了方法在同样的执行环境下可能抛出的其他任何异常.main方法例外.
对于方法可能抛出的未受检异常,将这些异常信息很好的组织成列表文档,可以有效地描述出这个方法被成功执行的前提条件.对于接口中的方法,尤为重要.
使用javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法声明中.
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档.
异常类型的toString方法应该尽可能多地返回有关失败原因地信息,异常的字符串表示法应该捕获失败,以便于后续进行分析.
为了捕获失败,异常的细节信息应该包含对该异常有贡献的所有参数和域的值.对于敏感信息,千万不要在细节消息中包含密码,密钥,以及类似的信息.
异常的细节消息主要让程序员来分析失败的原因,信息内容比可读性重要,而用户层次的错误消息则不同
为了确保在异常的细节消息中包含足够的失败-捕获信息,一种方法是在异常的构造器而不是字符串细节消息中引入这些信息.
一般而言,失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性.
最简单实现失败原子性的方法是设计一个不可变对象.
对于在可变对象上执行操作的方法, 获得失败原子性最常见的方法是:在执行操作前检查参数的有效性.与之类似的办法是:调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生.
第三种获得失败原子性的办法是:在对象的一份临时拷贝上执行操作,当操作完成之后,再用临时拷贝中的结果代替对象的内容.如果数据保存在临时的数据结构中,计算过程会更加迅速.
最后获得失败原子性的方法没那么常用, 做法是: 写一段恢复代码, 由它来拦截操作过程中发生的失败, 以及使对象回滚到操作开始之前的状态上.
虽然一般情况下都希望实现失败原子性,但并非总是可以做到.
方法产生的任何异常都应该让对象保持在调用该方法之前的状态, 如果不能, 则API文档应该清楚地指明对象会处于什么样的状态.
使用空的catch块会使异常达不到应有的目的.
有些情况可以忽略异常,比如关闭FileInputStream的时候.但是应该把异常记录下来.如果选择忽视异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored.建议同样适用于受检异常和未受检异常.
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块.同步不仅可以阻止一个线程看到对象处以不一致的状态之中,还可以保证进入同步方法或者同步代码块的每个线程都能看到由同一个锁保护的之前所有的修改效果.
Java语言规范保证读或写一个变量是原子的,除非这个变量的类型是long或double.但是不保证一个线程写入的值对另一个线程将是可见的.为了线程之间进行可靠的通信,也为了互斥访问,同步锁必要的.
public class TestSynchronized {
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;
}
}
上面的这段程序将永远不会停止,因为没有同步,主线程对stopRequested的修改,后台线程并没有看到.
没有同步,虚拟机将以下代码:
while (!stopRequested) {
i++;
}
变更为:
if (!stopRequested) {
while (true) {
i++;
}
}
这种优化叫做提升,结果导致活性失败.
修正上述问题,需在写方法和都方法都进行同步.注意: 除非读和写操作都被同步,否则无法保证同步能起到作用.
public class TestSynchronized {
private static boolean stopRequested;
private static synchronized boolean stopRequested() {
return stopRequested;
}
private static synchronized void requestStop() {
stopRequested = true;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested()) {
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
第二中修正方法是将stopRequested声明为volatile.虽然volatile修饰符不执行互斥访问,但可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值:
private static volatile boolean stopRequested;
但使用volatile要小心,下面的程序:
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
增量操作符++不是原子的,执行两个操作:首先读取值,然后写回一个新值.相当于原来的值再加上1.如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号,这就是安全性失败.
修正generateSerialNumber方法的一种方法是在它的声明中增加synchronized修饰符,应该删除nextSerialNumber的volatile修饰符.
但是最好使用java.until.concurrent.atomic中的AtomicLong类:
private static AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,它只同步共享对象引用的动作.然后其他线程没有进一步的同步也可以读取对象,只有它没有被修改.这种对象称作高效不可变.将这种对象引用从一个线程传递到其他线程被称作安全发布.
为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制.在一个被同步的区域内部, 不要调用设计成要覆盖的方法, 或是由客户端以函数对象的形式提供的方法.
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new ArrayList<>();
public void addObserver(SetObserver<E> observer) {
synchronized (observers) {
observers.add(observer);
}
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized (observers) {
return observers.remove(observer);
}
}
private void notifyElementAdded(E element) {
synchronized (observers) {
for (SetObserver<E> observer : observers) {
observer.added(this, element);
}
}
}
@Override
public boolean add(E e) {
boolean added = super.add(e);
if (added) {
notifyElementAdded(e);
}
return added;
}
@Override
public boolean addAll(Collection<? extends E> c) {
boolean result = false;
for (E element : c) {
result |= add(element);
}
return result;
}
}
public interface SetObserver<E> {
void added(ObservableSet<E> set, E element);
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
}
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((s, e) -> System.out.println(e));
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
假设用一个匿名类, 当Integer的元素值为23时,则这个观察者要将自身删除:
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if (element == 23) {
set.removeObserver(this);
}
}
});
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
程序运行后,打印出23后,抛出ConcurrentModificationException.问题是当notifyElementAdded调用观察者的added方法时,它正处于变量observers列表的过程中.added方法调用可观察集合的removeObserver方法,从而调用了observers.remove.因此造成了在遍历列表的过程中,将一个元素从列表中删除,这是非法的.
下面的例子改为使用另一个线程的服务来完成取消预订的观察者:
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if (element == 23) {
ExecutorService exec = Executors.newSingleThreadExecutor();
try {
exec.submit(() -> set.removeObserver(this)).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
});
上述代码没有异常,但遇到死锁,因set.removeObserver需要获取observers的锁,但observers的锁已经被主线程拥有.
解决方案是:
private void notifyElementAdded(E element) {
List<SetObserver<E>> snapshot = null;
synchronized (observers) {
snapshot = new ArrayList<>(observers);
}
for (SetObserver<E> observer : snapshot) {
observer.added(this, element);
}
}
除了将外来方法的调用移除同步的代码块,还可以使用并发集合CopyOnWriteArrayList.这个集合通过重新拷贝整个低层数组,实现所有的写操作.内部数组永远不改动,因此迭代不需要锁定.
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();
public void addObserver(SetObserver<E> observer) {
observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) {
return observers.remove(observer);
}
在同步区域之外被调用的外来方法称作开放调用,可以避免失败,还可以极大地增加并发性.
通常来说,应该在同步区域内做尽可能少的工作.
永远不要过度同步.过度同步的实际成本不是指获取锁所花费的CPU时间,而是指失去了并行的机会.以及因为需要确保每个核都有一个一致的内存视图而导致的延迟.过度同步会限制虚拟机优化代码的能力.
如果正在编写一个可变的类,有两种选择: 省略所有的同步, 如果想要并发使用,就允许客户端在必要的时候从外部同步(如非并发集合),或者通过内部同步,使这个类变成线程安全的(比如并发集合).
如果在内部同步了类, 可以使用不同的方法来实现高并发,比如分拆锁,分离锁,非阻塞并发控制.
如果方法修改了静态域,且该方法很可能被多个线程调用,那么也必须在内部同步对这个域的访问.
推荐使用Executor Framework.
ExecutorService exec = Executors.newSingleThreadExecutor();
// 执行一个runnable方法
exec.execute(runnable);
// 优雅的终止
exec.shutdown();
可利用executor service完成更多的工作:
如果编写的是小程序或者轻量负载的服务器,使用Executors.newCachedThreadPool.对于大负载的服务器,最好使用Executors.newFixedThreadPool或使用ThreadPoolExecutor最大限度控制.而不用缓存的线程池,因被提交的任务没有排成队列,而是直接交给线程执行.
不仅应该尽量不要编写自己的工作队列,而且应该尽量不直接使用线程.当直接使用Thread时,Thread既充当工作单元,又是执行机制.在Executor Framework中工作单元和执行机制是分开的.工作单元称为任务,有两种Runnable和Callable.执行任务的机制是executor service.
Java7之后支持fork-join任务,fork-join任务用ForkJoinTask实例表示,可以被分成更小的子任务,保护ForkJoinPool的线程不仅要处理这些任务,还要从另一个线程中偷任务,以确保所有的线程保持忙碌,从而提高CPU使用率,提高吞吐量,并降低延迟.
正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替.
java.util.concurrent中更高级的工具分为: Executor Framework, 并发集合, 同步器.
并发集合为标准的集合结果提高了高性能的并发实现,这些实现在内部自己管理同步.ConcurrentHashMap提高了卓越的并发性,且速度非常快.应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap.
有些集合接口以及通过阻塞操作进行可扩展,它们会一直等待(或者阻塞)直到可以成功执行为止.BlockingQueue扩展了Queue接口,并添加了take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空就等待,这样就允许将阻塞队列用于工作队列(生产者-消费者队列).大多数ExecutorService实现都使用了一个BlockingQueue.
同步器是使线程能够等待另一个线程的对象,允许它们协调动作.最常用的同步器是CountDownLatch和Semaphore.较不常用的是CyclicBarrier和Exchanger.功能最强大的同步器是Phaser.
倒计数锁存器(CountDownLatch)是一次性的障碍,允许一个或多个线程等待一个或多个其他线程来做某些事情.CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数.
下面的是CountDownLatch的示例程序:
public static long time(int concurrency, Runnable action) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
CountDownLatch ready = new CountDownLatch(concurrency);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(() -> {
ready.countDown();
try {
start.await();
action.run();
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
done.countDown();
}
});
}
ready.await();
long startNano = System.nanoTime();
start.countDown();
done.await();
executor.shutdown();
return System.nanoTime() - startNano;
}
上面的方法使用了三个倒计数锁存器,第一个是ready,工作线程用它来告诉time线程它们以及准备好了.然后工作线程在第二个锁存器start上等待.当最后一个工作线程调用read.countDown是,time线程记录下起始时间,并调用start.countDown,允许所有的工作线程继续进行,然后time线程在第三个锁存器done上等待,直到最后一个工作线程完成,并调用done.countDown.一旦调用这个, time线程就会苏醒过来,并记录结束的时间.
这里有些细节值得注意:
wait方法被用来使线程等待某个条件,它必须在同步区域内被调用,这个同步区域将对象锁定在了调用wait方法的对象上,使用wait的标准模式:
synchronized (obj) {
while (<condition dose not hold>) {
obj.wait();
......
}
}
始终应该使用wait循环模式来调用wait方法,永远不要在循环之外调用wait方法
应该始终使用notifyAll方法,notifyAll总会产生正确的结果,可以保证将会唤醒所有需要被唤醒的线程.
在一个方法声明中出现synchronized修饰符,是实现细节,不是导出API的一部分,不能表明这个方法是线程安全的.
一个类为了可被多个线程安全使用,必须在文档中清楚地说明它所支持的线程安全级别:
在文档中描述一个有条件的线程安全类要特别小心,必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁,通常情况下,是指作用在实例自身的那把锁,但Collections.synchronizedMap例外,当并发使用Map的集合视图时(即keySet方法返回的),应该以Map实例对象为锁.
私有锁对象需被声明为private final的防止客户端访问和修改,只能用在无条件的线程安全类上,有条件的线程安全类不能使用,因为它们必须在文档中说明,在执行某些方法调用序列时,客户端程序必须获得哪把锁.
私有锁对象模式特别适用于专门为继承而设计的类,如果这种类使用它的实例作为锁对象,子类可能很容易在无意中妨碍基类的操作,反之亦然.
延迟初始化是指延迟到需要域的值时才将它初始化的行为.最好建议: 除非绝对必要,否则就不要进行延迟初始化.它降低了初始化类或者创建实例的开销,但增加了访问被延迟初始化的域的开销.
大多数情况下,正常的初始化要优先于延迟初始化.
// 正常初始化
private final FieldType field = computeFieldValue();
// 延迟初始化,如果利用延迟优化初始化的循环,就要使用同步访问方法
private FieldType field;
private synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
return field;
}
// 静态域和实例域一样,不过域和方法声明需要添加static修饰符
如果出于性能的考虑而需要对静态域使用延迟初始化,则使用lazy initialization holder class模式,这种模式保证类要被用到的时候才会被初始化:
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field
}
当getField方法第一次被调用时,它第一次读取FieldHolder.field.导致FieldHolder类得到初始化.这种模式,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上没有增加任何访问成本,限度的VM将在初始化该类的时候,同步域的访问,一旦这个类被初始化,虚拟机将修补代码,以便后续对该域的访问不会导致任何测试或同步.
如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式.两次检查域的值,第一次检查时没有锁定,看看这个域是否被初始化了,第二次检查时有锁定,只有当第二次检查时表明这个域没有被初始化,才会对这个域进行初始化,如果域已经被初始化就不会有锁定,因此域需要被声明为volatile.
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;
}
有时可能需要延迟初始化一个可以接受重复初始化的实例域,则使用单重检查模式:
private volatile FieldType field;
private FieldType getField() {
// result这个变量的作用是确保field只在已经被初始化的情况下读取一次,可提升性能
FieldType result = field;
if (result == null) {
field = result = computeFieldValue();
}
return result;
}
任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的.
要编写出健壮,响应良好,可移植的多线程应用程序,最好的方法是确保可运行线程的平均数量不明显多于处理器的数量.
保持可运行线程数量尽可能少的主要方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作,如果线程没有在做有意义的工作,就不应该运行.
线程不应该一直处于忙-等状态,即反复的检查一个共享对象,以等待某些事情发生,除了使程序易受到调度器的变化影响之外,忙等这种做法也会极大地增加处理器的负担,降低了同一机器上其他进程可以完成的有用工作量.
不要企图通过Thread.yield来修正该程序, Thread.yield没有可测试的语义.
线程优先级是Java平台上最不可移植的特征了.
序列化的根本问题在于,其攻击面过于庞大,无法进行防护,并还在不断的扩大,对象图是通过ObjectInpurStream上调用readObject的方法进行反序列化的,这个方法可以将类路径上几乎任何类型的对象都实例化,只要该类型实现了Seriablizable接口.
避免序列化攻击的最佳方式是永远不要反序列化任何东西.
最前沿的跨平台结构化数据表示法是JSON和Protocol Buffers.JSON是为浏览器-服务器之间的通信设计的,Protocol Buffers是Google为了在服务器之间保持和交换结构化数据设计的.JSON是基于文本的,人是可以阅读的,protobuf是二进制的,更有效.
永远不要反序列化不被信任的数据,尤其是永远不应该接受来自不信任资源的RMI通信.
如果无法避免序列化,应利用Java9中新增的对象反序列化过滤,可以在数据流被反序列化之前,为它们定义一个过滤器,可以操作类的粒度,默认接受类,同时拒绝可能存在危险的黑名单;默认拒绝类,同时接受假定安全的白名单,白名单优于黑名单,因为黑名单只能抵御已知的攻击.
序列化是很危险的,应该予以避免,如果重新设计一个系统,一定要用跨平台的结构化数据表示法代替.
实现Serializable接口而付出的最大代价是一旦一个类被发布,就大大降低了改变这个类的实现的灵活性.一个类实现了Serializable接口则序列化形式就成了它的导出API的一部分,一旦这个类被广泛使用,往往必须永远支持这种序列化形式,不努力设计一种自定义的序列化形式,而仅仅接受了默认的序列化形式,这种序列化形式将被永远得束缚在该类最初得内部表示法上,可能导致不兼容.
序列化会使类得演变受到限制,序列化版本UID没有声明一个显式的,一旦改变,兼容性会被破坏.
实现Serializable接口的第二个代价是, 它增加了出现Bug和安全漏洞的可能性.序列化机制是一种语言之外的对象创建机制,反序列化必须也要保证所有由真正的构造器建立起来的约束关系.
实现Serializable接口的第三个代价是, 随着类发行新的版本,相关的测试负担会增加.需要检查是否可以在新版本序列化一个实例,然后在旧版本中反序列化.
为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少继承Serializable接口.
内部类不应该实现Serializable接口.内部类使用编译器产生的合成域来保存执行外围实例的引用,以及保存外围作用域的局部变量的值,这些域如何对应到类定义中没有明确的规定,因此内部类的默认序列化形式是定义不清楚的.但静态成员类却可以实现Serializable接口.
如果事先没有认真考虑默认的序列化形式是否合适,则不要贸然接受,接受默认的序列化形式需要从灵活性,性能和正确性等多个角度对这种编码形式进行考察.
对于一个对象来说, 理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的.
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式.
即使你确定了默认的序列化形式是合适的,通常还必要提供一个readObject方法保证约束关系和安全性.
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认的序列化形式的缺点:
writeObject方法的首要任务是调用defaultWriteObject, readObject方法的首要任务是调用defaultReadObject, 序列化规范要求你不管怎么样都要调用它们,这样得到的序列化形式允许在以后的发现版本中增加非瞬时的实例域,并且还能保持向前或者向后兼容性.
决定将一个域做出非瞬时(transient)之前,请一定要确信它的值将是该对象逻辑状态的一部分,如果你正在使用一种自定义的序列化形式,大多数实例域或者所有实例域都应该被标记为transient.
如果你正在使用默认的序列化形式,并且把一个或多个域标记为transient,则要记住,当一个实例被反序列化的时候,这些域被初始化为它们的默认值,如果默认值不能被任何transient域接受,你就必须提供一个readObject方法,它首先调用defaultReadObject,然后把transient域恢复为可接受的值.
无论是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步.
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID,这样可避免序列版本UID成为潜在的不兼容的根源.
readObject方法实际上是另一个公有构造器,同样要检查参数的有效性,并且在必要的时候进行保护性拷贝.readObject可以利用字节流创建一个不可能的对象.
当一个对象被反序列化的时候, 对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝.保护性拷贝是在有效性检查之前进行的,且不要使用clone方法进行拷贝.对于final域使用保护性拷贝是不可能的,为了使用readObject方法,必须将final域做出非final的.
readObject方法不可以调用可被子类覆盖的方法.
编写readObject方法的建议:
对于单例,一旦实现了Serializable接口就不再是单例.可通过反序列化返回一个和以前不同的新实例.
readResolve特性允许你用readObject创建的实例代替另一个实例.对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用,然后改方法返回的对象引用将被返回,取代新建的对象.在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,立即成为垃圾回收的对象.
如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient,否则就有可能在readResolve方法被运行之前,保护指向反序列化对象的引用.如果单例包含一个非瞬时的对象引用域,这个域的内容就可以在单例的readResolve方法运行之前被反序列化,当对象引用域的内容被反序列化时,它就允许一个精心制作的流盗用指向最初被反序列化的单例的引用.
如果将一个可序列化的实例受控的类编写成枚举,Java就可以绝对保证除了所声明的变量之外,不会有其他实例.
如果把readResolve方法放在一个final类上,它就应该是私有的.如果把readResolve方法放在一个非final类上,就必须考虑它的可访问性.如果readResolve方法是受保护的或者是公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化就会产生一个超类实例.有可能导致ClassCastException异常.
序列化代理模式: 首先为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类的实例的逻辑状态.这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是外围类.这个构造器只从它的参数中复制数据,且不需要进行任何一致性检查或者保护性拷贝.序列化代理的默认序列化形式是外围类最好的序列化形式.外围类及其序列代理都必须声明实现Serializable接口.
public class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date getStart() {
return new Date(this.start.getTime());
}
public Date getEnd() {
return new Date(this.end.getTime());
}
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
public SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID = 23456789L;
// 序列代理类中提供一个readResolve,返回一个逻辑上相当的外围类的实例.
// 这个方法导致序列化系统在反序列化时将序列化代理转变回外围类的实例
private Object readResolve() {
return new Period(start, end);
}
}
// 序列化生成一个SerializationProxy实例,代替外围类的实例
// writeReplace方法在序列化之前,将外围类的实例转变成了它的序列化代理
private Object writeReplace() {
return new SerializationProxy(this);
}
// 为了防止攻击者伪造外围类的序列化实例
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}
序列化代理模式让你不必单独确保反序列化地实例一定要遵守类地约束条件,因为外围类的静态工厂或者构造器建立了这些约束,只需在内部嵌套类中进行调用即可.
序列化代理模式允许反序列化实例有着与原始序列化实例不同的类.以EnumSet的情况为例,根据底层枚举类型的大写,如果底层枚举类型有64个或者小于64个元素,EnumSet的静态工厂则返回一个RegularEnumSet实例,否则返回一个JumboEnumSet实例.
如果序列化一个枚举集合,它的枚举类型有60个元素(已经序列化过的),然后给这个枚举类型在增加5个元素,之后反序列化这个枚举集合.当它被序列化的时候,是一个RegularEnumSet实例,但一旦它(增加新5个元素后)被反序列化,确实返回一个JumboEnumSet实例.
序列化代理模式有两个局限性: 它不能与任何被客户端扩展的类相兼容;也不能与对象图中包含循环的某些类相兼容.
序列化代理模式相比保护性拷贝,会增加序列化和反序列化的开销.