《OnJava8》精读(八)数组、枚举及注解

在这里插入图片描述

@

介绍


《On Java 8》是什么?

它是《Thinking In Java》的作者Bruce Eckel基于Java8写的新书。里面包含了对Java深入的理解及思想维度的理念。可以比作Java界的“武学秘籍”。任何Java语言的使用者,甚至是非Java使用者但是对面向对象思想有兴趣的程序员都该一读的经典书籍。目前豆瓣评分9.5,是公认的编程经典。

为什么要写这个系列的精读博文?

由于书籍读起来时间久,过程漫长,因此产生了写本精读系列的最初想法。除此之外,由于中文版是译版,读起来还是有较大的生硬感(这种差异并非译者的翻译问题,类似英文无法译出唐诗的原因),这导致我们理解作者意图需要一点推敲。再加上原书的内容很长,只第一章就多达一万多字(不含代码),读起来就需要大量时间。

所以,如果现在有一个人能替我们先仔细读一遍,筛选出其中的精华,让我们可以在地铁上或者路上不用花太多时间就可以了解这边经典书籍的思想那就最好不过了。于是这个系列诞生了。

一些建议

推荐读本书的英文版原著。此外,也可以参考本书的中文译版。我在写这个系列的时候,会尽量的保证以“陈述”的方式表达原著的内容,也会写出自己的部分观点,但是这种观点会保持理性并尽量少而精。本系列中对于原著的内容会以引用的方式体现。
最重要的一点,大家可以通过博客平台的评论功能多加交流,这也是学习的一个重要环节。

第二十一章 数组

本章总字数:19000
关键词:

  • 数组特性
  • 多维数组
  • 泛型数组
  • Arrays工具类

本章节在原著中作者建议可以略过或只是做简单了解。因为有了集合和流编程之后,数组的使用频率已经不那么多。但是依然可以作为了解的知识。

随着 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

Arrays工具类

常用的数组工具类:

  • asList(): 获取任何序列或数组,并将其转换为一个 列表集合 (集合章节介绍了此方法)。
  • copyOf():以新的长度创建现有数组的新副本。
  • copyOfRange():创建现有数组的一部分的新副本。
  • equals():比较两个数组是否相等。
  • deepEquals():多维数组的相等性比较。
  • stream():生成数组元素的流。
  • hashCode():生成数组的哈希值。
  • deepHashCode(): 多维数组的哈希值。
  • sort():排序数组
  • parallelSort():对数组进行并行排序,以提高速度。
  • binarySearch():在已排序的数组中查找元素。
  • toString():为数组生成一个字符串表示。
  • deepToString():为多维数组生成一个字符串。对于所有基本类型和对象,所有这些方法都是重载的。

第二十二章 枚举

本章总字数:14700
关键词:

  • 基本特性
  • 与switch搭配
  • EnumMap与多路分发

基本特性

在之前的章节已经有简单接触枚举。枚举可以认为是一组事物的简单分类。
比如:动物-》猫、狗、兔子

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搭配

枚举与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与多路分发

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
关键词:

  • 注解的定义
  • 自定义注解
  • 注解处理器
  • JUnit注解测试

注解的定义

C#语言中的“特性”概念允许程序员灵活的为方法或类添加额外的“标记”,这种标记往往更加方便就可以实现强大的功能。为了应对这种“其他语言能而Java不能”的情况,Java5诞生了注解。

注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像 C# 语言对于 Java 语言特性压力的一种回应。

Java中有5种注解,前三种是Java5引入:

  • @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
  • @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
  • @SuppressWarnings:关闭不当的编译器警告信息。
  • @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
  • @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口

注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解称为标记注解(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种元注解——元注解用于注解其他的注解

  • @Target 表示注解可以用于哪些地方。 可能的 ElementType 参数包括:
    CONSTRUCTOR:构造器的声明
    FIELD:字段声明(包括 enum 实例)
    LOCAL_VARIABLE:局部变量声明
    METHOD:方法声明
    PACKAGE:包声明
    PARAMETER:参数声明
    TYPE:类、接口(包括注解类型)或者 enum 声明
  • @Retention 表示注解信息保存的时长。 可选的 RetentionPolicy 参数包括:
    SOURCE:注解将被编译器丢弃
    CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。
    RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
  • @Documented 将此注解保存在 Javadoc 中
  • @Inherited 允许子类继承父类的注解
  • @Repeatable 允许一个注解可以被使用一次或者多次(Java 8)

大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。

自定义注解与注解处理器

作者告诉我们,注解在大部分时候都需要我们自己定义和使用。现有的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或相关代码才能实现一个简单特性。

posted @ 2021-01-23 21:20  Hi-Jimmy  阅读(404)  评论(0编辑  收藏  举报