《Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说: “如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。

本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

本文是系列的第三部分,包含对第六、七章的15个准则的解读,约1.9万字。

目录

  • 第六章 枚举和注解
    • 第34条:用枚举类型代替 int 常量
    • 第35条:使用实例字段替代序数
    • 第36条:用 EnumSet 替代位字段
    • 第37条:使用 EnumMap 替换序数索引
    • 第38条:使用接口模拟可扩展枚举
    • 第39条:注解优于命名模式
    • 第40条:坚持使用 @Override 注解
    • 第41条:使用标记接口定义类型
  • 第7章 λ表达式和流
    • 第42条:λ 表达式优于匿名类
    • 第43条:方法引用优于 λ 表达式
    • 第44条:优先使用标准函数式接口
    • 第45条:明智地使用流
    • 第46条:在流中使用无副作用的函数
    • 第47条:优先选择 Collection 而不是流作为返回类型
    • 第48条:谨慎使用并行流

第六章 枚举和注解

Chapter 6. Enums and Annotations

第34条:用枚举类型代替 int 常量

Item 34: Use enums instead of int constants

枚举类型相比int常量有不少优点,如:能提供类型安全性,能提供toString方法打印字符串,还允许添加任意方法和字段并实现任意接口,使得枚举成为功能齐全的抽象(富枚举类型)。

一般来说,枚举在性能上可与 int 常量相比,不过加载和初始化枚举类型需要花费空间和时间,实际应用中这一点可能不太明显。

第35条:使用实例字段替代序数

Item 35: Use instance fields instead of ordinals

所有枚举都有一个 ordinal 方法,该方法返回枚举类型中每个枚举常量的数值位置:

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {SOLO, DUET, TRIO, QUARTET, QUINTET,SEXTET, SEPTET, OCTET, NONET, DECTET;public int numberOfMusicians() { return ordinal() + 1; }
}

这样写虽然可行,但难以维护。如果常量被重新排序,numberOfMusicians 方法将被破坏。
更好的办法是使用一个额外的字段来代表序数:

public enum Ensemble {SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),NONET(9), DECTET(10),TRIPLE_QUARTET(12);private final int numberOfMusicians;Ensemble(int size) { this.numberOfMusicians = size; }public int numberOfMusicians() { return numberOfMusicians; }
}

ordinal是为基于枚举的通用数据结构(EnumSet 和 EnumMap)设计的。除非你用到这些数据结构,否则最好完全避免使用这个方法。

第36条:用 EnumSet 替代位字段

Item 36: Use EnumSet instead of bit fields

如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式,通过不同的 2 的平方数为每个常量赋值:

// Bit field enumeration constants - OBSOLETE!
public class Text {public static final int STYLE_BOLD = 1 << 0; // 1public static final int STYLE_ITALIC = 1 << 1; // 2public static final int STYLE_UNDERLINE = 1 << 2; // 4public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8// Parameter is bitwise OR of zero or more STYLE_ constantspublic void applyStyles(int styles) { ... }
}

这种表示方式称为位字段,允许你使用位运算的或操作将几个常量组合成一个 Set:

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

位字段具有 int 枚举常量所有缺点,例如:

  1. 被打印成数字时难以理解
  2. 没有简单的方法遍历位字段所有元素
  3. 一旦确定了int或long作为位字段的存储类型,就不能超过它的范围(32位或64位)

EnumSet类是一种更好的选择,它避免了以上缺点。而且由于它在底层实现上与位操作类似,因此与位字段性能相当。

将之前的示例修改为使用EnumSet的方法,更加简单清晰:

// EnumSet - a modern replacement for bit fields
public class Text {public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }// Any Set could be passed in, but EnumSet is clearly bestpublic void applyStyles(Set<Style> styles) { ... }
}

下面是将 EnumSet 实例传递给 applyStyles 方法的用户代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建 Set:

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

第37条:使用 EnumMap 替换序数索引

Item 37: Use EnumMap instead of ordinal indexing

用序数索引数组不如使用 EnumMap ,应尽量少使用 ordinal()

例如这个类表示一种植物:

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;}
}

假设有一个代表花园全部植物的 Plant 数组plantsByLifeCycle,用于列出按生命周期(一年生、多年生或两年生)排列的植物:

// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =(Set<Plant>[]) new Set[Plant.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);// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {System.out.printf("%s: %s%n",Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

这种技术有如下问题:

  1. 数组与泛型不兼容,需要 unchecked 转换。
  2. 数组不知道索引表示什么,必须手动标记输出。
  3. int 不提供枚举的类型安全性,无法检验int值的正确性

有一种更好的方法是使用 java.util.EnumMap

// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =new EnumMap<>(Plant.LifeCycle.class);for (Plant.LifeCycle lc : Plant.LifeCycle.values())plantsByLifeCycle.put(lc, new HashSet<>());for (Plant p : garden)plantsByLifeCycle.get(p.lifeCycle).add(p);System.out.println(plantsByLifeCycle);

这个程序比原来的版本更短,更清晰,更安全,速度也差不多。速度相当的原因是,EnumMap 在内部使用这样的数组,但是它向程序员隐藏了实现细节。

使用流可以进一步缩短程序:

// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));

这段代码的性能较差,因为底层不是基于 EnumMap,而是自己实现Map。要改进性能,可以使用 mapFactory 参数指定 Map 实现:

// Using a stream and an EnumMap to associate data with an enum
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle,() -> new EnumMap<>(LifeCycle.class), toSet()))
);

下面例子用二维数组描述了气液固三台之间的装换,并使用序数索引读取二维数组的值:

// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase {SOLID, LIQUID, GAS;public enum Transition {MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;// Rows indexed by from-ordinal, cols by to-ordinalprivate static final Transition[][] TRANSITIONS = {{ null, MELT, SUBLIME },{ FREEZE, null, BOIL },{ DEPOSIT, CONDENSE, null }};// Returns the phase transition from one phase to anotherpublic static Transition from(Phase from, Phase to) {return TRANSITIONS[from.ordinal()][to.ordinal()];}}
}

这个程序的问题与前面 garden 示例一样,编译器无法知道序数和数组索引之间的关系。如果你在转换表中出错,或者在修改 Phase 或 Phase.Transition 枚举类型时忘记更新,程序将在运行时失败。

使用 EnumMap 可以做得更好:

// Using a nested EnumMap to associate data with enum pairs
public enum Phase {SOLID, LIQUID, GAS;public enum Transition {MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);private final Phase from;private final Phase to;Transition(Phase from, Phase to) {this.from = from;this.to = to;}// Initialize the phase transition mapprivate static final Map<Phase, Map<Phase,Transition> m =new EnumMap<Phase, Map<Phase ,Transition>>(Phase.class);static{for (Phase p : Phase. values())m.put(p,new EnumMap<Phase,Transition (Phase.class));for (Transition trans : Transition.values() )m.get(trans. src).put(trans.dst, trans) ;}public static Transition from(Phase src, Phase dst) {return m.get(src).get(dst);}
}

如果你想向系统中加入一种新阶段:等离子体。这个阶段只有两个变化:电离、去离子作用。修改基于EnumMap的版本要比基于数组的版本容易得多,而且更加清晰安全:

// Adding a new phase using the nested EnumMap implementation
public enum Phase {SOLID, LIQUID, GAS, PLASMA;public enum Transition {MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);... // Remainder unchanged}
}

第38条:使用接口模拟可扩展枚举

Item 38: Emulate extensible enums with interfaces

虽然不能编写可扩展枚举类型,但是可以通过编写接口来模拟它。

Java语言层面不支持可扩展枚举类型,但有时我们需要实现类似的需求,如操作码。操作码是一种枚举类型,其元素表示某些机器上的操作,例如第34条中的 Operation 类,它表示简单计算器上的函数。有时候,我们希望 API 的用户提供自己的操作,从而有效地扩展 API 提供的操作集。

一种思路是为操作码类型定义一个接口,并为接口的标准实现定义一个枚举:

// Emulated extensible enum using an interface
public interface Operation {double apply(double x, double y);
}public enum BasicOperation implements Operation {PLUS("+") {public double apply(double x, double y) { return x + y; }},MINUS("-") {public double apply(double x, double y) { return x - y; }},TIMES("*") {public double apply(double x, double y) { return x * y; }},DIVIDE("/") {public double apply(double x, double y) { return x / y; }};private final String symbol;BasicOperation(String symbol) {this.symbol = symbol;}@Overridepublic String toString() {return symbol;}
}

用这种方法可以轻松扩展自己的实现:

// Emulated extension enum
public enum ExtendedOperation implements Operation {EXP("^") {public double apply(double x, double y) {return Math.pow(x, y);}},REMAINDER("%") {public double apply(double x, double y) {return x % y;}};private final String symbol;ExtendedOperation(String symbol) {this.symbol = symbol;}@Overridepublic String toString() {return symbol;}
}

通过传入扩展枚举类型到方法,可以编写一个上述代码的测试程序,它执行了前面定义的所有扩展操作:

public static void main(String[] args) {double x = Double.parseDouble(args[0]);double y = Double.parseDouble(args[1]);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())System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}

第二个选择是传递一个 Collection<? extends Operation>,而非类对象:

public static void main(String[] args) {double x = Double.parseDouble(args[0]);double y = Double.parseDouble(args[1]);test(Arrays.asList(ExtendedOperation.values()), x, y);
}private static void test(Collection<? extends Operation> opSet,double x, double y) {for (Operation op : opSet)System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}

这种方法的优点是:代码更简单,且允许调用者组合来自多个实现类型的操作。缺点是:放弃了在指定操作上使用 EnumSet和EnumMap的能力。

接口模拟可扩展枚举的一个小缺点是实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现将其放置在接口中。

第39条:注解优于命名模式

Item 39: Prefer annotations to naming patterns

如果可以使用注解,那么就没有理由使用命名模式。

历史上,使用命名模式来标明某些程序元素需要框架特殊处理是很常见的。例如,JUnit 4以前的版本要求其用户通过以字符 test 开头的名称来指定测试方法。

这种技术是有效的,但是有几个很大的缺点:

  1. 排版错误会导致没有提示的失败,导致一种正确执行了测试的假象。
  2. 无法在类的级别使用test命名模式。例如,命名一个类 为TestSafetyMechanisms,希望 JUnit 3 能够自动测试它的所有方法,是行不通的。
  3. 没有提供将参数值与程序元素关联的好方法。例如,希望支持只有在抛出特定异常时才成功的测试类别。如果将异常类型名称编码到测试方法名称中,那么代码将不好看且脆弱。

JUnit 从版本 4 开始使用注解解决了上述问题。在本条目中,我们将编写自己的示例测试框架来展示注解是如何工作的:

// Marker annotation type declaration
import java.lang.annotation.*;/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

@Retention(RetentionPolicy.RUNTIME) 元注解表明测试注解在运行时生效。@Target.get(ElementType.METHOD) 元注解表明测试注解仅对方法生效。

Test的注释说明它只能用于无参的静态方法,但实际上编译器并没有对此做强制。

下面是 Test 注解实际使用时的样子。如果程序员拼错 Test 或将 Test 注解应用于除方法声明之外的元素,程序将无法编译:

// Program containing marker annotations
public class Sample {@Testpublic static void m1() { } // Test should passpublic static void m2() { }@Testpublic static void m3() { // Test should failthrow new RuntimeException("Boom");}public static void m4() { }@Testpublic void m5() { } // INVALID USE: nonstatic methodpublic static void m6() { }@Testpublic static void m7() { // Test should failthrow new RuntimeException("Crash");}public static void m8() { }
}

Sample 类有 7 个静态方法,其中 4 个被注解为 Test。其中两个方法 m3 和 m7 抛出异常,另外两个 m1 和 m5 没有抛出异常。但是,m5 不是静态方法,所以不是有效的用例。总之,Sample 包含四个测试用例:一个通过,两个失败,一个无效。

以下是解析并运行Test 注解标记的测试的例子:

// Program to process marker annotations
import java.lang.reflect.*;public class RunTests {public static void main(String[] args) throws Exception {int tests = 0;int passed = 0;Class<?> testClass = Class.forName(args[0]);for (Method m : testClass.getDeclaredMethods()) {if (m.isAnnotationPresent(Test.class)) {tests++;try {m.invoke(null);passed++;} catch (InvocationTargetException wrappedExc) {Throwable exc = wrappedExc.getCause();System.out.println(m + " failed: " + exc);} catch (Exception exc) {System.out.println("Invalid @Test: " + m);}}}System.out.printf("Passed: %d, Failed: %d%n",passed, tests - passed);}
}

如果在 Sample 上运行 RunTests,输出如下:

public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
Passed: 1, Failed: 3

现在让我们添加一个只在抛出特定异常时才成功的测试支持。需要一个新的注解类型:

// Annotation type with a parameter
import java.lang.annotation.*;/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {Class<? extends Throwable> value();
}

下面是这个注解的实际应用:

// Program containing annotations with a parameter
public class Sample2 {@ExceptionTest(ArithmeticException.class)public static void m1() { // Test should passint i = 0;i = i / i;}@ExceptionTest(ArithmeticException.class)public static void m2() { // Should fail (wrong exception)int[] a = new int[0];int i = a[1];}@ExceptionTest(ArithmeticException.class)public static void m3() { } // Should fail (no exception)
}

修改RunTests类来处理新的注解。向 main 方法添加以下代码:

if (m.isAnnotationPresent(ExceptionTest.class)) {tests++;try {m.invoke(null);System.out.printf("Test %s failed: no exception%n", m);} catch (InvocationTargetException wrappedEx) {Throwable exc = wrappedEx.getCause();Class<? extends Throwable> excType =m.getAnnotation(ExceptionTest.class).value();if (excType.isInstance(exc)) {passed++;} else {System.out.printf("Test %s failed: expected %s, got %s%n",m, excType.getName(), exc);}}catch (Exception exc) {System.out.println("Invalid @Test: " + m);}
}

这段代码提取注解参数的值,并使用它来检查测试抛出的异常是否是正确的类型。

进一步修改异常测试示例,将允许的指定异常扩展到多个:

// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {Class<? extends Exception>[] value();
}

以下是对应的测试用例:

// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,NullPointerException.class })
public static void doublyBad() {List<String> list = new ArrayList<>();// The spec permits this method to throw either// IndexOutOfBoundsException or NullPointerExceptionlist.addAll(5, null);
}

修改RunTests类:

if (m.isAnnotationPresent(ExceptionTest.class)) {tests++;try {m.invoke(null);System.out.printf("Test %s failed: no exception%n", m);} catch (Throwable wrappedExc) {Throwable exc = wrappedExc.getCause();int oldPassed = passed;Class<? extends Exception>[] excTypes =m.getAnnotation(ExceptionTest.class).value();for (Class<? extends Exception> excType : excTypes) {if (excType.isInstance(exc)) {passed++;break;}}if (passed == oldPassed)System.out.printf("Test %s failed: %s %n", m, exc);}
}

Java 8 中可以在注解声明上使用 @Repeatable (重复注解)达到类似效果,提升程序可读性:

// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {Class<? extends Exception> value();
}@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {ExceptionTest[] value();
}

使用重复注解代替数组值注解的测试用例:

// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }

为重复注解版本对应修改RunTests类:

// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class)|| m.isAnnotationPresent(ExceptionTestContainer.class)) {tests++;try {m.invoke(null);System.out.printf("Test %s failed: no exception%n", m);} catch (Throwable wrappedExc) {Throwable exc = wrappedExc.getCause();int oldPassed = passed;ExceptionTest[] excTests =m.getAnnotationsByType(ExceptionTest.class);for (ExceptionTest excTest : excTests) {if (excTest.value().isInstance(exc)) {passed++;break;}}if (passed == oldPassed)System.out.printf("Test %s failed: %s %n", m, exc);}
}

第40条:坚持使用 @Override 注解

Item 40: Consistently use the Override annotation

在每个方法声明上都使用 @Override 注解来覆盖超类型声明,编译器可以帮助减少有害错误的影响。

下面的类Bigram表示一个二元有序的字母对:

// Can you spot the bug?
public class Bigram {private final char first;private final char second;public Bigram(char first, char second) {this.first = first;this.second = second;}public boolean equals(Bigram b) {return b.first == first && b.second == second;}public int hashCode() {return 31 * first + second;}public static void main(String[] args) {Set<Bigram> s = new HashSet<>();for (int i = 0; i < 10; i++)for (char ch = 'a'; ch <= 'z'; ch++)s.add(new Bigram(ch, ch));System.out.println(s.size());}
}

主程序重复地向一个集合中添加 26 个 bigram,每个 bigram 由两个相同的小写字母组成。然后它打印该集合的大小。运行该程序,它打印的不是 26 而是 260。

Bigram 类的作者打算覆盖 equals 方法,但实际上重载了它,因为参数类型不同。这个继承来的 equals 方法只能检测对象同一性,就像 == 操作符一样。每组中的每个 bigram 副本都不同于其他 9 个,这就解释了为什么程序最终打印 260。

如果使用Override注解就可以避免这个错误:

@Override
public boolean equals(Bigram b) {return b.first == first && b.second == second;
}

这时编译器将生成如下错误消息:

Bigram.java:10: method does not override or implement a method from a supertype
@Override public boolean equals(Bigram b) {^

再修改为正确的实现:

@Override
public boolean equals(Object o) {if (!(o instanceof Bigram))return false;Bigram b = (Bigram) o;return b.first == first && b.second == second;
}

因此,应该在要覆盖超类声明的每个方法声明上使用 @Override 注解。只有一个例外,如果你正在编写一个非抽象类,并且它覆盖了其超类中的抽象方法,那么不一定需要添加 @Override 注解。

第41条:使用标记接口定义类型

Item 41: Use marker interfaces to define types

标记接口是一种不包含任何方法声明的接口,它只是标记它的实现类具有某种特性。如Serializable 接口,实现此接口的类可以写入ObjectOutputStream(被序列化)。

如果要定义类型,那么使用标记接口优于使用标记注解。

标记接口相比标记注解有两个优点:

  1. 标记接口定义的类型由标记类的实例实现;标记注解不会。前者在编译时捕获错误,后者在运行时才能捕捉错误。
  2. 可以更精确地定位。标记注解可以应用于任何类或接口,而标记接口限定于它的实现类。

标记注解的优点是:它们可以是其他注解功能的一部分。

总之,如果你想要定义一个没有与之关联的新方法的类型,可以使用标记接口。如果你希望标记类和接口之外的程序元素,或者将标记符放入已经大量使用注解类型的框架中,那么应该使用标记注解。

第7章 λ表达式和流

第42条:λ 表达式优于匿名类

Item 42: Prefer lambdas to anonymous classes

表式小函数对象时,lambda表达式优于匿名类。

带有单个抽象方法的接口称为函数类型,它们的实例称为函数对象。历史上,创建函数对象的主要方法是匿名类。下面是一段按长度对字符串列表进行排序的代码,使用一个匿名类来创建排序的比较函数:

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {public int compare(String s1, String s2) {return Integer.compare(s1.length(), s2.length());}
});

匿名类的缺点是代码过于冗长。

在 Java 8 中,将具有单个抽象方法的接口称为函数式接口,允许使用 lambda 表达式创建这些接口的实例。Lambda 表达式更加简洁:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));

在上面的lambda 表达式中看不到对参数及其返回值类型的定义,这是因为类型是由编译器使用类型推断从上下文中推理出来的。

关于类型推断,需要注意:

  1. 第26条:不要使用原始类型。
  2. 第29条:优先使用泛型。
  3. 第30条:优先使用泛型方法。

这些建议都是为了方便编译器从泛型中获取类型推断所需的信息。否则需要在lambda表达式中手动指定类型,这会使代码更加冗长。

使用 comparator 构造方法代替 lambda 表达式,可以让代码变得更加简洁:

Collections.sort(words, comparingInt(String::length));

通过 Java 8 中添加到 List 接口的 sort 方法,可以使代码变得更短:

words.sort(comparingInt(String::length));

我们可以使用lambda表达式优化第34条中的操作枚举类型,以下是原代码:

// Enum type with constant-specific class bodies & data (Item 34)
public enum Operation {PLUS("+") {public double apply(double x, double y) { return x + y; }},MINUS("-") {public double apply(double x, double y) { return x - y; }},TIMES("*") {public double apply(double x, double y) { return x * y; }},DIVIDE("/") {public double apply(double x, double y) { return x / y; }};private final String symbol;Operation(String symbol) { this.symbol = symbol; }@Overridepublic String toString() { return symbol; }public abstract double apply(double x, double y);
}

使用lambda表达式优化为可读性更好的版本:

// Enum with function object fields & constant-specific behavior
public 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 symbol; }public double apply(double x, double y) {return op.applyAsDouble(x, y);}
}

其中 DoubleBinaryOperator 接口是 java.util.function 中预定义的函数式接口之一。它表示一个函数,接收两个 double 类型参数,返回值也为 double 类型。

lambda表达式并非任何场合都适合,以下情况就不适合:

  1. 算法较难理解时。写在带有名字会更有助于理解。
  2. 代码行数过多时。一般三行是合理的最大值。
  3. 需要在lambda表达式中访问枚举实例成员时。无法访问,因为传递给enum构造函数的参数在静态上下文中计算。

有一些匿名类可以做的事情是 lambda 表达式不能做的:

  1. 创建抽象类的实例。Lambda表达式仅限于函数式接口。
  2. 创建具有多个抽象方法的接口实例。
  3. 获得对自身的引用。在 lambda 表达式中,this 关键字指的是外层实例,而非lambda表达式本身。

第43条:方法引用优于 λ 表达式

Item 43: Prefer method references to lambdas

方法引用通常比lambda 表达式更简洁,应优先使用。

下面一个程序的代码片段的功能是:如果数字 1 不在映射中,则将其与键关联,如果键已经存在,则将关联值递增:

map.merge(key, 1, (count, incr) -> count + incr);

在 Java 8 中,Integer类提供了一个静态方法 sum,它的作用完全相同,且更简单:

map.merge(key, 1, Integer::sum);

如果 lambda 表达式太长或太复杂,那么可以将代码从 lambda 表达式提取到一个新方法中,并以对该方法的引用替换 lambda 表达式。可以为该方法起一个好名字并将其文档化。

有时候,lambda 表达式也会比方法引用更简洁。当方法与 lambda 表达式在同一个类中时,如以下代码片段发生在一个名为 GoshThisClassNameIsHumongous 的类中:

service.execute(GoshThisClassNameIsHumongous::action);

使用 lambda 表达式会更简洁:

service.execute(() -> action());

许多方法引用指向静态方法,但是有四种方法不指向静态方法。其中两个是绑定和非绑定实例方法引用。在绑定引用中,接收对象在方法引用中指定。在未绑定引用中,在应用函数对象时通过方法声明参数之前的附加参数指定接收对象。最后,对于类和数组,有两种构造函数引用。五种类型的方法汇总如下表:

方法引用类型 例子 等价的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]

第44条:优先使用标准函数式接口

Item 44: Favor the use of standard functional interfaces

自从 Java 已经有了 lambda 表达式,编写 API 的最佳实践变化很大。例如,以前会用子类覆盖基类方法以实现多态,现代的替代方法是提供一个静态工厂或构造函数,它接受一个函数对象来实现相同的效果。

例如 LinkedHashMap。可以通过覆盖受保护的 removeEldestEntry 方法将该类用作缓存,每当向映射添加新键时,put 都会调用该方法。当该方法返回 true 时,映射将删除传递给该方法的最老条目。下面的覆盖保证在每次添加新键时删除最老的条目,维护 100 个最近的条目:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return size() > 100;
}

如果使用 lambda 表达式改造,那么LinkedHashMap将有一个静态工厂或构造函数,它接受一个函数对象,函数对象实现的函数式接口如下:

// Unnecessary functional interface; use a standard one instead.
@FunctionalInterface interface EldestEntryRemovalFunction<K,V>{boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

上面对于新接口的声明不是必需的,因为java.util.function 包已经提供了大量的标准函数接口。如果一个标准的函数式接口可以完成这项工作,那么你通常应该优先使用它,而不是使用专门构建的函数式接口。 在LinkedHashMap 示例中,应该优先使用标准的 BiPredicate<Map<K,V>Map.Entry<K,V>> 接口。

java.util.function 中有 43 个接口。可以只用记住 6 个基本接口,其余的接口在需要时派生出来:

  1. Operator 接口:表示结果和参数类型相同的函数。(根据参数数量可分为一元、二元)
  2. Predicate 接口:表示接受参数并返回布尔值的函数。
  3. Function 接口:表示参数和返回类型不同的函数。
  4. Supplier 接口:表示一个不接受参数并返回值的函数。
  5. Consumer 接口:表示一个函数,该函数接受一个参数,但不返回任何内容,本质上是使用它的参数。

六个基本的函数式接口总结如下:

接口 方法签名 例子
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Consumer<T> void accept(T t) System.out::println

大多数标准函数式接口的存在只是为了提供对基本类型的支持。例如,一个接受 int 的 Predicate 就是一个 IntPredicate,一个接受两个 long 值并返回一个 long 的二元操作符就是一个 LongBinaryOperator。不要尝试用带有包装类的基本函数式接口替代它们,因为这会带来糟糕的性能。

如果标准的函数式接口都不能满足需求,那么需要自行编写,例如,如果你需要一个接受三个参数的 Predicate,或者一个抛出已检查异常的 Predicate。

但是有时候即使其中一个标准接口在结构上满足需求,也应该编写自己的函数接口。以 Comparator<T>为例,它在结构上与 ToIntBiFunction<T,T> 接口相同。有几个原因前者优于后者:

  1. Comparator的名称提供了优秀的文档,且使用频繁。
  2. 通过实现接口,保证遵守其约定。
  3. 该接口拥有大量用于转换和组合比较器的有用默认方法。

如果需要一个与 Comparator 共享以下特性的函数式接口,那么应该考虑编写一个专用的函数式接口,而不是使用标准接口:

  1. 将被广泛使用,并且从描述性名称中获益。
  2. 有一个严格的约定。
  3. 受益于自定义默认方法。

建议总是用 @FunctionalInterface 注释你的函数接口。
有三个好处:

  1. 告诉文档读者,接口的设计是为了启用 lambda 表达式。
  2. 除非只有一个抽象方法,否则编译会报错。
  3. 防止维护者在接口发展过程中意外地向接口添加抽象方法。

第45条:明智地使用流

Item 45: Use streams judiciously

根据具体场合决定使用流还是迭代。

在 Java 8 中添加了流 API,以简化序列或并行执行批量操作的任务。其中有两个关键的抽象:流(表示有限或无限的数据元素序列)和流管道(表示对这些元素的多阶段计算)。流中的数据元素可以是对象的引用或基本数据类型。支持三种基本数据类型:int、long 和 double。

流管道由源流、零个或多个中间操作(intermediate)和一个终止操作(terminal )组成。每个中间操作以某种方式转换流,例如将每个元素映射到该元素的一个函数,或者过滤掉不满足某些条件的所有元素。终止操作做最终计算,例如将其元素存储到集合中、返回特定元素、或打印其所有元素。

流管道的计算是惰性的:直到调用终止操作时才开始计算,并且对完成终止操作不需要的数据元素永远不会计算。注意,没有终止操作的流管道是无动作的,因此不要忘记包含一个终止操作。

如果使用得当,流可以使程序更短、更清晰;否则,它们会降低程序可读性。

下面的程序从字典文件中读取单词并打印所有大小满足用户指定最小值的变位词组。如果两个单词以不同的顺序由相同的字母组成,那么它们就是变位词,属于同一个变位词组:

// Prints all large anagram groups in a dictionary iteratively
public class Anagrams {public static void main(String[] args) throws IOException {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);}private static String alphabetize(String s) {char[] a = s.toCharArray();Arrays.sort(a);return new String(a);}
}

将每个单词插入到 Map 中使用了computeIfAbsent方法:如果键存在,那么该方法仅返回与其关联的值;否则,该方法通过将给定的函数对象应用于键来计算一个值,并将该键值对放置到Map中。

将其修改成使用流的版本:

// Overuse of streams - don't do this!
public class Anagrams {public static void main(String[] args) throws IOException {Path dictionary = Paths.get(args[0]);int minGroupSize = Integer.parseInt(args[1]);try (Stream<String> words = Files.lines(dictionary)) {words.collect(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);}}
}

上面代码难以阅读,原因是过度使用了流。我们减少一些非必要使用流的地方,优化成如下代码:

// Tasteful use of streams enhances clarity and conciseness
public class Anagrams {public static void main(String[] args) throws IOException {Path dictionary = Paths.get(args[0]);int minGroupSize = Integer.parseInt(args[1]);try (Stream<String> words = Files.lines(dictionary)) {words.collect(groupingBy(word -> alphabetize(word))).values().stream().filter(group -> group.size() >= minGroupSize).forEach(g -> System.out.println(g.size() + ": " + g));}}// alphabetize method is the same as in original version
}

注意,参数 g 实际上应该命名为 group,但是生成的代码太长,在没有显式类型的情况下,lambda 表达式参数的谨慎命名对于流管道的可读性至关重要。另外,单词的字母化是在一个单独的字母化方法中完成的,好处是将实现细节排除在主程序之外,从而增强可读性。

使用流处理 char 值有风险,应尽量避免。例如下面代码:

"Hello world!".chars().forEach(System.out::print);

你可能希望它打印 Hello world!,但实际打印 721011081081113211911111410810033。这是因为 "Hello world!".chars() 返回的流元素不是 char 值,而是 int 值,因此调用了 print 的 int 重载。可以通过强制调用正确的重载来修复:

"Hello world!".chars().forEach(x -> System.out.print((char) x));

迭代使用的是代码块,而流通常使用的是lambda表达式或方法引用。有些事情可以在代码块中做,却不能在lambda表达式中做:

  1. 读取或修改作用域中的任何局部变量。lambda 表达式中不能修改局部变量。
  2. 从封闭方法返回、中断或继续封闭循环。
  3. 抛出声明要抛出的任何已检查异常。

流擅长做的事情:

  1. 元素序列的统一变换。
  2. 过滤元素序列。
  3. 使用单个操作组合元素序列(例如添加、连接或计算它们的最小值)。
  4. 将元素序列累积到一个集合中,可能是按某个公共属性对它们进行分组。
  5. 在元素序列中搜索满足某些条件的元素。

使用流很难做到的一件事是从管道的多个阶段同时访问相应的元素:一旦你将一个值映射到另一个值,原始值就会丢失。一个解决方案是将每个值映射到包含原始值和新值的 pair 对象,但这会让代码更加冗长。更好的解决方案是在需要访问早期阶段值时反转映射。

例如,编写一个程序来打印前 20 个 Mersenne 素数。一个 Mersenne 素数的数量是一个数字形式 2p − 1。如果 p 是素数,对应的 Mersenne 数可以是素数;如果是的话,这就是 Mersenne 素数。作为管道中的初始流,我们需要所有质数:

static Stream<BigInteger> primes() {return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

下面是打印前 20 个 Mersenne 素数的程序:

public static void main(String[] args) {primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).filter(mersenne -> mersenne.isProbablePrime(50)).limit(20).forEach(System.out::println);
}

现在假设我们想要在每个 Mersenne 素数之前加上它的指数,这个值只在初始流中存在。幸运的是,通过对第一个中间操作中发生的映射求逆,可以很容易地计算出 Mersenne 数的指数:

.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));

在许多任务中,使用流还是迭代并不明显。例如,考虑初始化一副新纸牌的任务。假设 Card 是一个不可变的值类,它封装了 Rank 和 Suit,它们都是 enum 类型。此任务代表需要计算可从两个集合中选择的所有元素对的任何任务,也称为这两个集合的笛卡尔积:

// Iterative Cartesian product computation
private static List<Card> newDeck() {List<Card> result = new ArrayList<>();for (Suit suit : Suit.values())for (Rank rank : Rank.values())result.add(new Card(suit, rank));return result;
}

下面是一个基于流的实现:

// Stream-based Cartesian product computation
private static List<Card> newDeck() {return Stream.of(Suit.values()).flatMap(suit ->Stream.of(Rank.values()).map(rank -> new Card(suit, rank))).collect(toList());
}

两个版本的 newDeck 哪个更好?这个只能说见仁见智了。

第46条:在流中使用无副作用的函数

Item 46: Prefer side effect free functions in streams

应保证传递到流操作(包括中间操作和终止操作)中的任何函数对象都应该没有副作用。

下面代码用于构建文本文件中单词的频率表:

// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {words.forEach(word -> {freq.merge(word.toLowerCase(), 1L, Long::sum);});
}

上面代码根本不是流代码,而是伪装成流代码的迭代代码,而且比普通迭代代码更冗长。原因是这段代码在一个 终止操作中(forEach)执行它的所有工作,这是一种不当用法,终止操作本应只用来收集计算结果。正确的写法应该是:

// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {freq = words.collect(groupingBy(String::toLowerCase, counting()));
}

改进后的代码使用了 收集器(collector),它是用来收集计算结果的集合。收集器有三种:toList()toSet()toCollection(collectionFactory),分别返回 List、Set 和程序员指定的集合类型。下面代码用收集器从 freq 表中提取前 10 个元素来构成一个新 List。

// Pipeline to get a top-ten list of words from a frequency table
List<String> topTen = freq.keySet().stream().sorted(comparing(freq::get).reversed()).limit(10).collect(toList());

最简单的 Map 收集器是 toMap(keyMapper, valueMapper),它接受两个函数,一个将流元素映射到键,另一个映射到值。我们在第34条中的 fromString 实现中使用了该收集器来创建枚举的字符串形式到枚举本身的映射:

// Using a toMap collector to make a map from string to enum
private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));

注意:需保证流中的每个元素映射到唯一的键。如果多个流元素映射到同一个键,管道将以 IllegalStateException 结束。

toMap 的三参数形式,允许为toMap 方法提供一个 merge 函数。例如,从有一个由不同艺术家录制的唱片流,可以得到一个从艺术家到最畅销唱片的映射:

// Collector to generate a map from key to chosen element for key
Map<Artist, Album> topHits = albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales))
));

toMap 的三参数形式的另一个用途是,当发生键冲突时,它强制执行后写覆盖的策略。例如:

// Collector to impose last-write-wins policy
toMap(keyMapper, valueMapper, (v1, v2) -> v2)

toMap 也提供了四参数的版本,当你想要指定一个特定的 Map 实现(如 EnumMap 或 TreeMap)时,可以使用它。

还有前三个版本的 toMap 的变体形式,名为 toConcurrentMap,它们可以有效地并行运行,同时生成 ConcurrentHashMap 实例。

收集器API 还提供 groupingBy 方法,该方法生成基于分类器函数将元素分组为类别的映射。例如下面代码:

words.collect(groupingBy(word -> alphabetize(word)))

groupingBy也提供两参数形式,将 counting() 作为下游收集器传递。这将生成一个 Map,它将每个类别与类别中的元素数量相关联:

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

另一个收集器方法是 join,它只对 CharSequence 实例流(如字符串)执行操作。它接受一个名为 delimiter 的 CharSequence 参数,然后在相邻元素之间插入分隔符。如果传入逗号作为分隔符,收集器将返回逗号分隔的值字符串。

第47条:优先选择 Collection 而不是流作为返回类型

Item 47: Prefer Collection to Stream as a return type

如果一个 API 只返回一个流,而一些用户希望使用 for-each 循环遍历返回的序列,那么这些用户将会感到困难。例如:

// Won't compile, due to limitations on Java's type inference
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {// Process the process
}

编译时会报错:

Test.java:6: error: method reference not expected here
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {^

解决方法是将方法引用转换为适当参数化的 Iterable:

// Hideous workaround to iterate over a stream
for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)

上面的代码太繁琐,更好的解决方案是使用适配器方法:

// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {return stream::iterator;
}

通过这个适配器可以使用 for-each 语句遍历流:

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {// Process the process
}

同样,如果返回的是迭代器,而希望使用的是流,那么也需要编写适配器:

// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {return StreamSupport.stream(iterable.spliterator(), false);
}

如果考虑到作为公共API对外提供,那么建议使用Collection作为方法返回值。Collection 接口是 Iterable 的一个子类型,它有一个流方法,因此它提供了迭代和流两种访问方式。

假设你想要返回给定集合的幂集,该集合由它的所有子集组成。例如{a, b, c} 的排列组合有 {{}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}。下面是代码:

// Returns the power set of an input set as custom collection
public class PowerSet {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>>() {@Overridepublic int size() {return 1 << src.size(); // 2 to the power srcSize}@Overridepublic boolean contains(Object o) {return o instanceof Set && src.containsAll((Set)o);}@Overridepublic 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;}};}
}

另一个例子是实现一个输入列表的所有子列表的流:

// Returns a stream of all the sublists of its input list
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()));}
}

我们的子列表实现在本质上类似于嵌套的 for 循环:

for (int start = 0; start < src.size(); start++)for (int end = start + 1; end <= src.size(); end++)System.out.println(src.subList(start, end));

结论是,在编写返回元素序列的方法时,遵循以下建议:

  1. 如果可以返回集合,那么就这样做。
  2. 如果你已经在一个集合中拥有了元素,或者序列中的元素数量足够小,可以创建一个新的元素,那么返回一个标准集合,例如 ArrayList 。
  3. 否则像对 power 集合那样实现自定义集合。
  4. 如果返回集合不可行,则返回流或 iterable,以看起来更自然的方式返回。

第48条:谨慎使用并行流

Item 48: Use caution when making streams parallel

绝大多数情况下,不要并行化流管道,除非通过测试证明它能保持计算的正确性以及提高性能。

编写正确且快速的并发程序是件困难的事情。考虑第45条中打印素数的程序:

// Stream-based program to generate the first 20 Mersenne primes
public static void main(String[] args) {primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).filter(mersenne -> mersenne.isProbablePrime(50)).limit(20).forEach(System.out::println);
}static Stream<BigInteger> primes() {return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

假设尝试通过向流管道添加 parallel() 来加速它,性能会有提升吗?结果是它不会打印任何东西,但是 CPU 使用率会飙升到 90%,并且会无限期地停留在那里(活跃性失败)。

原因是stream 库不知道如何并行化这个管道。如果源来自 Stream.iterate 或使用中间操作限制,并行化管道不太可能提高其性能。

并行性能带来明显性能提升的场合:

  1. ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例
  2. 数组
  3. 有界的IntStream和LongStream

这些数据结构的共同之处是:

  1. 可以被精确且廉价地分割成任意大小的子结构,这使得在并行线程之间划分工作变得很容易。
  2. 顺序处理时提供了良好的引用局部性(locality of reference)。引用局部性是指,当一个存储位置被处理器访问时,短时间内这个位置及附近位置被重复访问的趋势。良好的引用局部性可以充分利用处理器的多级缓存,带来性能提升。

流管道的终止操作的性质也会影响并行执行的效果。如果在终止操作中做了大量工作,且操作是顺序的,那么并行带来的提升很有限。并行的最佳终止操作是缩减型的方法,如reduce、min、max、count和sum。短路操作anyMatch、allMatch 和 noneMatch 也适用于并行。collect 方法执行的操作称为可变缩减,它们不是并行的好候选,因为组合集合的开销是昂贵的。

并行化流使用的函数对象还需要遵守一些规范,否则可能导致不正确的结果。这些规范例如:传递给流的reduce 操作的累加器和组合器函数必须是关联的、不干扰的和无状态的。

并行化流必须经过严格的性能测试,确保带来的收益超过代价。一般的经验是,流中的元素数量乘以每个元素执行的代码行数至少应该是100000。

某些领域,如机器学习和数据处理,特别适合使用并行流。例如下面代码:

// Prime-counting stream pipeline - parallel version
static long pi(long n) {return LongStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
}

5天带你读完《Effective Java》(三)相关推荐

  1. 读完 Effective Java,我整理了 59 条技巧!(含pdf)

    点击⬆️方"逆锋起笔",公众号回复 编程资源领取大佬们推荐的学习资料 上一篇:CTO 写低级 Bug,致公司 70 GB 数据泄露! 作者:Dong GuoChao 链接:http ...

  2. 5天带你读完《Effective Java》(一)

    <Effective Java>是Java开发领域无可争议的经典之作,连Java之父James Gosling都说:"如果说我需要一本Java编程的书,那就是它了".它 ...

  3. 读完《Effective Java》后,总结了 50 条开发技巧

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | Dong GuoChao 来源 | https ...

  4. Effective Java第三版有哪些新功能?

    自从听说即将出版的有效Java 第三版以来,我一直想知道其中有什么新内容. 我假设将涵盖自Java 6以来引入Java的功能,的确如此. 但是,第三版Java开发人员经典版也有一些其他更改. 在本文中 ...

  5. Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25

    Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25 目录 Rule20 接口优于抽象类 Rule21 为后代设计接口 Rule22 接口只用于定义类型 ...

  6. Java之父James Gosling鼎力推荐《Effective Java 第三版》最新中文版,Java程序员必看神书

    前言 Java之父James Gosling鼎力推荐.Jolt获奖作品全新升级,针对Java 7.8.9全面更新,Java程序员必备参考书.包含大量完整的示例代码和透彻的技术分析,通过90条经验法则, ...

  7. 简单一文带你读懂Java变量的作用和三要素

    Java变量的作用 不只是java,在其他的编程语言中变量的作用只有一个:存储值(数据) 在java中,变量本质上是一块内存区域,数据存储在java虚拟机(JVM)内存中 变量的三要素 变量的三要素分 ...

  8. Effective Java笔记第五章枚举和注解第三节用EnumSet代替位域

    Effective Java笔记第五章枚举和注解 第三节用EnumSet代替位域 在以前如果一个枚举类型的元素主要用在集合中,一般就会使用int枚举模式.比如说: public class Demo ...

  9. Effective Java读书笔记三:创建和销毁对象

    第1条:考虑用静态工厂方法代替构造器 对于类而言,为了让客服端获得它的一个实例最常用的的一个方法就是提供一个公有的构造器.还有一种方法,类可以提供一个公有的静态工厂方法(static factory ...

最新文章

  1. 共话数据智能新经济,首届市北·GMIS 2019全球数据智能峰会隆重召开
  2. oracle 如何添加数据文件,Oracle教程 误添加数据文件删除方法
  3. 邮件的一个推送这个系统怎么去搭建的摘抄:感觉有很多的开源的邮件服务器可以参考使用搭建,据说wordpress有集成服务
  4. Android Service与Runnable整合并用
  5. ASP.NET 开源导入导出库Magicodes.IE 导出Pdf教程
  6. 记录一些常见的沟通问题 #29
  7. 在web应用程序中使用MemcachedClient
  8. android 获取手机信息工具类
  9. 牛客2022年愚人节比赛,10题做法完整版
  10. TensorFlow 中 tf.app.flags.FLAGS 的用法介绍
  11. 等待事件之日志等待事件解决的方法
  12. 用python实现遗传算法
  13. 超市系统服务器,超市收银系统 服务器 配置
  14. 创建Date对象的几种方式
  15. 为Windows 10 UWP 应用设置代理
  16. AVAudioPlayer 播放本地音乐
  17. 1、解读中台 -- 什么是中台
  18. 用c语言编写程序相似性检测,程序代码相似性检测在论文抄袭判定中的应用
  19. 一体化3团队项目记录
  20. 计算机教师继续教育心得,教师继续教育培训个人心得体会(精选6篇)

热门文章

  1. 【Linux】一种局域网内通信方法
  2. 经典名言+经典配色图
  3. AppUI自动化测试效率提升方案
  4. 四川峰创教育咨询有限公司:目前跨境电商适合做的类目有哪些?
  5. 手动获取音悦台MV下载地址方法
  6. xp系统运行php,window_Xp系统安装或运行软件时提示“EXE不是有效Win32应用程序”的故障原因及解决方法,部分Xp系统用户在双击安装某 - phpStudy...
  7. 2021年高考 南宁二中成绩查询,2021年南宁高考各高中成绩及本科升学率数据排名及分析...
  8. 测试人遇到难以重现的bug,要怎么办?
  9. 24个基本指标精粹讲解(13)--DMI
  10. Unreal Engine 4 问题:使用asio后编译打包报错:unresolved external symbol