它是《Thinking In Java》的作者Bruce Eckel基于Java8写的新书。里面包含了对Java深入的理解及思想维度的理念。可以比作Java界的“武学秘籍”。任何Java语言的使用者,甚至是非Java使用者但是对面向对象思想有兴趣的程序员都该一读的经典书籍。目前豆瓣评分9.5,是公认的编程经典。
由于书籍读起来时间久,过程漫长,因此产生了写本精读系列的最初想法。除此之外,由于中文版是译版,读起来还是有较大的生硬感(这种差异并非译者的翻译问题,类似英文无法译出唐诗的原因),这导致我们理解作者意图需要一点推敲。再加上原书的内容很长,只第一章就多达一万多字(不含代码),读起来就需要大量时间。
所以,如果现在有一个人能替我们先仔细读一遍,筛选出其中的精华,让我们可以在地铁上或者路上不用花太多时间就可以了解这边经典书籍的思想那就最好不过了。于是这个系列诞生了。
推荐读本书的英文版原著。此外,也可以参考本书的中文译版。我在写这个系列的时候,会尽量的保证以“陈述”的方式表达原著的内容,也会写出自己的部分观点,但是这种观点会保持理性并尽量少而精。本系列中对于原著的内容会以引用的方式体现。
最重要的一点,大家可以通过博客平台的评论功能多加交流,这也是学习的一个重要环节。
本章总字数:19000
关键词:
本章节在原著中作者建议可以略过或只是做简单了解。因为有了集合和流编程之后,数组的使用频率已经不那么多。但是依然可以作为了解的知识。
随着 Java Collection 和 Stream 类中高级功能的不断增加,日常编程中使用数组的需求也在变少,所以你暂且可以放心地略读甚至跳过这一章。但是,即使你自己避免使用数组,也总会有需要阅读别人数组代码的那一天。那时候,本章依然在这里等着你来翻阅。
即便集合更加灵活,但是数组依然有存在的必要。首先是效率——“在 Java 中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。”
但是较快的速度牺牲了灵活性。数组的大小是固定的,且在它的生命周期内无法更改。
Java中使用大括号嵌套来表示多维。在声明时可以使用多个中括号对多维声明长度。
int[][] a = {
{
1, 2, 3, },
{
4, 5, 6, },
};
int[][][] a1 = new int[2][2][4];
int[][][] a2 = new int[3][][];
System.out.println(Arrays.deepToString(a));
System.out.println(Arrays.deepToString(a1));
System.out.println(Arrays.deepToString(a2));
结果:
[[1, 2, 3], [4, 5, 6]]
[[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]]
[null, null, null]
数组可以与泛型结合形成泛型数组。
class ClassParameter<T> {
public T[] f(T[] arg) {
return arg; }
}
class MethodParameter {
public static <T> T[] f(T[] arg) {
return arg; }
}
...
Integer[] ints = {
1, 2, 3, 4, 5 };
Double[] doubles = {
1.1, 2.2, 3.3, 4.4, 5.5 };
Integer[] ints2 = new ClassParameter<Integer>().f(ints);
Double[] doubles2 = new ClassParameter<Double>().f(doubles);
但是注意,由于Java中泛型是用泛型擦除方式实现的,所以不能使用以下方式实例化:
House<Cat>[] c= new House<Cat>[10]; // Error
常用的数组工具类:
本章总字数:14700
关键词:
在之前的章节已经有简单接触枚举。枚举可以认为是一组事物的简单分类。
比如:动物-》猫、狗、兔子
public enum Animal {
cat,
dog,
rabbit
}
与C#不同,Java中的枚举不能直接赋值,也不能枚举之间相互赋值。这点C#较为灵活,详细可以参考这里。
Java中的枚举除了不能被继承外,与其他普通类没有区别,所以我们甚至可以为枚举类写方法或是覆盖方法。
public enum Animal {
cat,
dog,
rabbit;
String getCat() {
return "cat";
}
String getDog() {
return "dog";
}
String getRabbit() {
return "rabbit";
}
@Override
public String toString() {
return getCat() + getDog() + getRabbit();
}
}
枚举与switch搭配能起到分类判断的效果。这种写法往往比if-else 更清晰。
Animal a=Animal.cat;
switch (a){
case cat:
System.out.println(a.getCat());
break;
case dog:
System.out.println(a.getDog());
break;
case rabbit:
System.out.println(a.getRabbit());
break;
default:
System.out.println(a.toString());
break;
}
EnumMap与 Map相似,只是 key必须是枚举。以下是原著的一个例子,使用了嵌套 EnumMap方式实现猜拳游戏。
// enums/RoShamBo5.java
// Multiple dispatching using an EnumMap of EnumMaps
// {java enums.RoShamBo5}
package enums;
import java.util.*;
import static enums.Outcome.*;
public enum Outcome {
WIN, LOSE, DRAW }
public interface Competitor<T extends Competitor<T>> {
Outcome compete(T competitor);
}
public class RoShamBo {
public static <T extends Competitor<T>>
void match(T a, T b) {
System.out.println(
a + " vs. " + b + ": " + a.compete(b));
}
public static <T extends Enum<T> & Competitor<T>>
void play(Class<T> rsbClass, int size) {
for(int i = 0; i < size; i++)
match(Enums.random(rsbClass),Enums.random(rsbClass));
}
}
enum RoShamBo5 implements Competitor<RoShamBo5> {
PAPER, SCISSORS, ROCK;
static EnumMap<RoShamBo5,EnumMap<RoShamBo5,Outcome>>
table = new EnumMap<>(RoShamBo5.class);
static {
for(RoShamBo5 it : RoShamBo5.values())
table.put(it, new EnumMap<>(RoShamBo5.class));
initRow(PAPER, DRAW, LOSE, WIN);
initRow(SCISSORS, WIN, DRAW, LOSE);
initRow(ROCK, LOSE, WIN, DRAW);
}
static void initRow(RoShamBo5 it,
Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
EnumMap<RoShamBo5,Outcome> row = RoShamBo5.table.get(it);
row.put(RoShamBo5.PAPER, vPAPER);
row.put(RoShamBo5.SCISSORS, vSCISSORS);
row.put(RoShamBo5.ROCK, vROCK);
}
@Override
public Outcome compete(RoShamBo5 it) {
return table.get(this).get(it);
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo5.class, 20);
}
}
这个例子在RoShamBo5 类中使用 EnumMap实现了二路分发。这样就可以保证出两种手势得出一个结果。
本章总字数:14600
关键词:
C#语言中的“特性”概念允许程序员灵活的为方法或类添加额外的“标记”,这种标记往往更加方便就可以实现强大的功能。为了应对这种“其他语言能而Java不能”的情况,Java5诞生了注解。
注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像 C# 语言对于 Java 语言特性压力的一种回应。
Java中有5种注解,前三种是Java5引入:
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解称为标记注解(marker annotation)
一个使用注解的范例:
// annotations/Testable.java
package annotations;
import onjava.atunit.*;
public class Testable {
public void execute() {
System.out.println("Executing..");
}
@Test
void testExecute() {
execute(); }
}
被注解标注的方法和其他的方法没有任何区别。在这个例子中,注解 @Test 可以和任何修饰符共同用于方法,诸如 public、static 或 void。从语法的角度上看,注解的使用方式和修饰符的使用方式一致。
Java中有5种元注解——元注解用于注解其他的注解。
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。
作者告诉我们,注解在大部分时候都需要我们自己定义和使用。现有的Java框架中,有不少自定义注解。比如我们常用的Spring框架,Hibernate框架以及Mybatis等。这些随框架而来的各种各样注解都需依赖各自的注解处理器。
以下是一个简单的自定义注解:
// annotations/UseCase.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
参照上文的元注解,我们可以理解该注解是一个适用于方法,可以使用反射读取注解信息的自定义注解。
接下来接可以使用自定义注解:
// annotations/PasswordUtils.java
import java.util.*;
public class PasswordUtils {
@UseCase(id = 47, description =
"Passwords must contain at least one numeric")
public boolean validatePassword(String passwd) {
return (passwd.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)
public String encryptPassword(String passwd) {
return new StringBuilder(passwd)
.reverse().toString();
}
@UseCase(id = 49, description =
"New passwords can't equal previously used ones")
public boolean checkForNewPassword(
List<String> prevPasswords, String passwd) {
return !prevPasswords.contains(passwd);
}
}
注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 @UseCase 声明之后的括号内。在 encryptPassword() 方法的注解中,并没有给出 description 的值,所以在 @interface UseCase 的注解处理器分析处理这个类的时候会使用该元素的默认值。
以上的范例只是标记了自定义注解,但是没有被执行。没有被执行的注解是没有意义的。现在需要一个注解处理器来对注解标记进行处理:
// annotations/UseCaseTracker.java
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
public class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("Found Use Case " +
uc.id() + "\n " + uc.description());
useCases.remove(Integer.valueOf(uc.id()));
}
}
useCases.forEach(i ->
System.out.println("Missing use case " + i));
}
public static void main(String[] args) {
List<Integer> useCases = IntStream.range(47, 51)
.boxed().collect(Collectors.toList());
trackUseCases(useCases, PasswordUtils.class);
}
}
结果:
Found Use Case 48
no description
Found Use Case 47
Passwords must contain at least one numeric
Found Use Case 49
New passwords can't equal previously used ones
Missing use case 50
这个程序用了两个反射的方法:getDeclaredMethods() 和 getAnnotation(),它们都属于 AnnotatedElement 接口(Class,Method 与 Field 类都实现了该接口)。getAnnotation() 方法返回指定类型的注解对象,在本例中就是 “UseCase”。如果被注解的方法上没有该类型的注解,返回值就为 null。我们通过调用 id() 和 description() 方法来提取元素值。注意 encryptPassword() 方法在注解的时候没有指定 description 的值,因此处理器在处理它对应的注解时,通过 description() 取得的是默认值 “no description”。
Java中最常用的单元测试就是JUnit。 使用 @Test注解可以很方便的测试代码。
// annotations/AUComposition.java
// Creating non-embedded tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AUComposition.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AUComposition {
AtUnitExample1 testObject = new AtUnitExample1();
@Test
boolean tMethodOne() {
return testObject.methodOne()
.equals("This is methodOne");
}
@Test
boolean tMethodTwo() {
return testObject.methodTwo() == 2;
}
}
结果:
annotations.AUComposition
. tMethodTwo This is methodTwo
. tMethodOne
OK (2 tests)
我们使用 @Test 来标记测试方法。测试方法不带参数,并返回 boolean 结果来说明测试方法成功或者失败。你可以任意命名它的测试方法。同时 @Unit 测试方法可以是任意你喜欢的访问修饰方法,包括 private。
本篇已经接近原著的尾声,数组和枚举是概念性内容,而注解是本篇的关键。可以说是注解给了Java更强大的发展能力。如果没有注解,我们现在可能得写大量的xml或相关代码才能实现一个简单特性。