《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或相關代碼才能實現一個簡單特性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章