《Java編程思想》讀書筆記(四)

前言:三年之前就買了《Java編程思想》這本書,但是到現在爲止都還沒有好好看過這本書,這次希望能夠堅持通讀完整本書並整理好自己的讀書筆記,上一篇文章是記錄的第十七章到第十八章的內容,這一次記錄的是第十九章到第二十章的內容,相關示例代碼放在碼雲上了,碼雲地址:https://gitee.com/reminis_com/thinking-in-java

第十九章:枚舉類型

關鍵字enum可以將一組具名的值的有限集合創建爲一種新的類型,而這些具名的值可以作爲常規的程序組件使用,這是一種非常有用的功能。

enum的基本特性

  我們已經知道,調用enum的values()方法,可以遍歷enum實例。values()方法返回enum實例的數組,而且該數組中的元素嚴格保持其在enum中聲明時的順序,因此你可以在循環中使用values()返回的數組。
  創建enum時,編譯器會爲你生成一個相關的類,這個類繼承自java.lang.Enum。下面的例子演示了Enum提供的一些功能∶

package enumerated;

/**
 * @author Mr.Sun
 * @date 2022年09月02日 15:58
 *
 * 枚舉的基本特性
 */
public class EnumClass {
    public static void main(String[] args) {
        for(Shrubbery s : Shrubbery.values()) {
            System.out.println(s + " ordinal: " + s.ordinal());
            System.out.print(s.compareTo(Shrubbery.CRAWLING) + " ");
            System.out.print(s.equals(Shrubbery.CRAWLING) + " ");
            System.out.println(s == Shrubbery.CRAWLING);
            System.out.println(s.getDeclaringClass());
            System.out.println(s.name());
            System.out.println("----------------------");
        }

        // 從字符串名稱生成枚舉值
        for(String s : "HANGING CRAWLING GROUND".split(" ")) {
            Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);
            System.out.println(shrub);
        }
    }
}

enum Shrubbery {
    GROUND, CRAWLING, HANGING
}

運行結果如下圖:

  ordinal()方法返回一個int值,這是每個enum實例在聲明時的次序,從0開始。可以使用==來比較enum實例,編譯器會自動爲你提供equals()和hashCode()方法。Enum類實現了Comparable 接口,所以它具有compareTo()方法。同時,它還實現了Serializable接口。
  如果在enum實例上調用getDeclaringClass()方法,我們就能知道其所屬的enum類。name()方法返回enum實例聲明時的名字,這與使用toString()方法效果相同。valueOf()是在Enum中定義的static方法,它根據給定的名字返回相應的enum實例,如果不存在給定名字的實例,將會拋出異常。

values()的神祕之處

  前面已經提到,編譯器爲你創建的enum類都繼承自Enum類。然而,如果你研究一下Enum 類就會發現,它並沒有values()方法。可我們明明已經用過該方法了,難道存在某種“隱藏的”方法嗎?我們可以利用反射機制編寫一個簡單的程序,來查看其中的究竟∶

package enumerated;

import utils.OSExecute;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author Mr.Sun
 * @date 2022年09月02日 16:10
 *
 * 使用反射機制研究枚舉類的values()
 */
enum Explore {
    HERE, THERE
}

public class Reflection {

    public static Set<String> analyze(Class<?> enumClass) {
        System.out.println("----- Analyzing " + enumClass + " -----");
        System.out.println("Interfaces:");
        for (Type t : enumClass.getGenericInterfaces()) {
            System.out.println(t);
        }
        System.out.println("Base: " + enumClass.getSuperclass());
        System.out.println("Methods: ");
        Set<String> methods = new TreeSet<String>();
        for (Method method : enumClass.getMethods()) {
            methods.add(method.getName());
        }
        System.out.println(methods);
        return methods;
    }

    public static void main(String[] args) {
        Set<String> exploreMethods = analyze(Explore.class);
        Set<String> enumMethods = analyze(Enum.class);
        System.out.println("Explore.containsAll(Enum)? " + exploreMethods.containsAll(enumMethods));
        System.out.print("Explore.removeAll(Enum): ");
        exploreMethods.removeAll(enumMethods);
        System.out.println(exploreMethods);
        // Decompile the code for the enum:
        OSExecute.command("javap G:/github/cnblogs/gitee/thinking-in-java/out/production/thinking-in-java/enumerated/Explore.class");
    }

} /* Output:
----- Analyzing class enumerated.Explore -----
Interfaces:
Base: class java.lang.Enum
Methods: 
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, values, wait]
----- Analyzing class java.lang.Enum -----
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods: 
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
Compiled from "Reflection.java"
final class enumerated.Explore extends java.lang.Enum<enumerated.Explore> {
  public static final enumerated.Explore HERE;
  public static final enumerated.Explore THERE;
  public static enumerated.Explore[] values();
  public static enumerated.Explore valueOf(java.lang.String);
  static {};
}
*///:~

  答案是,values()是由編譯器添加的static方法。可以看出,在創建Explore的過程中,編譯器還爲其添加了valueOf()方法。這可能有點令人迷惑,Enum類不是已經有valueOf()方法了嗎。不過Enum中的valueOf()方法需要兩個參數,而這個新增的方法只需一個參數。由於這裏使用的Set只存儲方法的名字,而不考慮方法的簽名,所以在調用Explore.removeAl(Enum)之後,就只剩下【values】了。
  從最後的輸出中可以看到,編譯器將Explore標記爲final類,所以無法繼承自enum。其中還有一個static的初始化子句,稍後我們將學習如何重定義該句。
  由於擦除效應(在第15章中介紹過),反編譯無法得到Enum的完整信息,所以它展示的Explore的父類只是一個原始的Enum,而非事實上的Enum
  由於values()方法是由編譯器插入到enum定義中的static方法,所以,如果你將enum實例向上轉型爲Enum,那麼values()方法就不可訪問了。不過,在Class中有一個getEnumConstants()方法,所以即便Enum接口中沒有values()方法,我們仍然可以通過Class對象取得所有enum實例∶

enum Search {
    HITHER, YON
}


public class UpcastEnum {
    public static void main(String[] args) {
        Search[] values = Search.values();
        for (Search val : values) {
            System.out.println(val.name());
        }
        System.out.println("--------------");
        Enum e = Search.HITHER;
        for (Enum en : e.getClass().getEnumConstants()) {
            System.out.println(en);
        }
    }
}

運行結果如下:

使用EnumSet代替標誌

  Set是一種集合,只能向其中添加不重複的對象。當然,enum也要求其成員都是唯一的,所以enum看起來也具有集合的行爲。不過,由於不能從enum中刪除或添加元素,所以它只能算是不太有用的集合。Java SE5引入EnumSet,是爲了通過enum創建一種替代品,以替代傳統的"基於int的“位標誌”。這種標誌可以用來表示某種“開/關”信息,不過,使用這種標誌,我們最終操作的只是一些bit,而不是這些bit想要表達的概念,因此很容易寫出令人難以理解的代碼。

  EnumSet的設計充分考慮到了速度因素,因爲它必須與非常高效的bit標誌相競爭(其操作與HashSet相比,非常地快)。就其內部而言,它(可能)就是將一個long值作爲比特向量,所以EnumSet非常快速高效。使用EnumSet的優點是,它在說明一個二進制位是否存在時,具有更好的表達能力,並且無需擔心性能。EnumSet中的元素必須來自一個enum。下面的enum表示在一座大樓中,警報傳感器的安放位置∶

package enumerated;

public enum AlarmPoints {
    STAIR1, STAIR2,
    LOBBY,
    OFFICE1, OFFICE2, OFFICE3, OFFICE4,
    BATHROOM, UTILITY, KITCHEN
}

然後,我們使用EnumSet來跟蹤報警器的狀態:

package enumerated;

import java.util.EnumSet;

import static enumerated.AlarmPoints.*;

/**
 * @author Mr.Sun
 * @date 2022年09月02日 17:18
 */
public class EnumSetTest {
    public static void main(String[] args) {
        EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class); // Empty set
        points.add(BATHROOM);
        System.out.println(points);

        points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);

        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);

        points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
        System.out.println(points);

        points = EnumSet.complementOf(points);
        System.out.println(points);
    }
}

運行結果如下圖:

使用EnumMap

  EnumMap是一種特殊的Map,它要求其中的鍵(key)必須來自一個enum。由於enum本身的限制,所以EnumMap在內部可由數組實現。因此EnumMap的速度很快,我們可以放心地使用enum實例在EnumMap中進行查找操作。不過,我們只能將enum的實例作爲鍵來調用put()方法,其他操作與使用一般的Map差不多。
  下面的例子演示了命令設計模式的用法。一般來說,命令模式首先需要一個只有單一方法的接口,然後從該接口實現具有各自不同的行爲的多個子類。接下來,程序員就可以構造命令對象,並在需要的時候使用它們了∶

package enumerated;

import java.util.EnumMap;
import java.util.Map;

import static enumerated.AlarmPoints.*;

/**
 * @author Mr.Sun
 * @date 2022年09月02日 17:25
 *
 * 使用EnumMap
 */
interface Command{ void action(); }

public class EnumMapTest {
    public static void main(String[] args) {
        EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
        em.put(KITCHEN, () -> System.out.println("Kitchen fire!"));
        em.put(BATHROOM, () -> System.out.println("Bathroom alert!"));
        for(Map.Entry<AlarmPoints,Command> e : em.entrySet()) {
            System.out.print(e.getKey() + ": ");
            e.getValue().action();
        }
        try {
            // If there's no value for a particular key:
            em.get(UTILITY).action();
        } catch(Exception e) {
            System.out.println(e);
        }
    }
}/* Output:
BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
java.lang.NullPointerException
*///:~

  與EnumSet一樣,enum實例定義時的次序決定了其在EnumMap中的順序。
  main()方法的 最後部分說明,enum的每個實例作爲一個鍵,總是存在的,但是如果你沒有爲這個鍵調用put()方法來存入相應的值的話,對應的值就是null。

常量相關的方法

  Java的Enum有一個非常有趣的特性,即它允許程序員爲enum實例編寫方法,從而爲每個enum實例賦予各自不同的行爲,要實現常量相關的方法,你需要爲enum定義一個或多個abstract方法,然後爲每個enum實例實現該抽象方法。參考下面的例子:

package enumerated;

import java.text.DateFormat;
import java.util.Date;

public enum ConstantSpecificMethod {

    DATE_TIME {
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }
    },
    CLASSPATH {
        String getInfo() {
            return System.getenv("CLASSPATH");
        }
    },
    VERSION {
        String getInfo() {
            return System.getProperty("java.version");
        }
    };

    abstract String getInfo();

    public static void main(String[] args) {
        for(ConstantSpecificMethod csm : values()) {
            System.out.println(csm.getInfo());
        }
    }
}/* Output:
2022-9-2
null
1.8.0_211
*///:~

使用enum的職責鏈

  在職責鏈(Chain of Responsibility)設計模式中,程序員以多種不同的方式來解決一個問題,然後將它們鏈接在一起。當一個請求到來時,它遍歷這個鏈,直到鏈中的某個解決方案能夠處理該請求。

  通過常量相關的方法,我們可以很容易地實現一個簡單的職責鏈。我們以一個郵局的模型爲例。郵局需要以儘可能通用的方式來處理每一封郵件,並且要不斷嘗試處理郵件,直到該郵件最終被確定爲一封死信。其中的每一次嘗試可以看作爲一個策略(也是一個設計模式),而完整的處理方式列表就是一個職責鏈。
  我們先來描述一下郵件。郵件的每個關鍵特徵都可以用enum來表示。程序將隨機地生成Mail對象,如果要減小一封郵件的GeneralDelivery爲YES的概率,那最簡單的方法就是多創建幾個不是YES的enum實例,所以enum的定義看起來有點古怪。
  我們看到Mail中有一個randomMail()方法,它負責隨機地創建用於測試的郵件。而generator()方法生成一個Iterable對象,該對象在你調用next()方法時,在其內部使用randomMail()來創建Mail對象。這樣的結構使程序員可以通過調用Mail.generator()方法,很容易地構造出一個foreach循環∶

package enumerated;

import utils.Enums;

import java.util.Iterator;

/**
 * @author Mr.Sun
 * @date 2022年09月02日 17:45
 *
 * 以郵局的模型爲例,通過常量相關的方法,實現一個簡單的職責鏈
 */
public class PostOffice {

    enum MailHandler {
        GENERAL_DELIVERY {
            boolean handle(Mail m) {
                switch (m.generalDelivery) {
                    case YES:
                        System.out.println("Using general delivery for " + m);
                        return true;
                    default:
                        return false;
                }
            }
        },

        MACHINE_SCAN {
            boolean handle(Mail m) {
                switch (m.scannability) {
                    case UNSCANNABLE:
                        return false;
                    default:
                        switch (m.address) {
                            case INCORRECT:
                                return false;
                            default:
                                System.out.println("Delivering " + m + " automatically");
                                return true;
                        }
                }
            }
        },
        VISUAL_INSPECTION {
            boolean handle(Mail m) {
                switch (m.readability) {
                    case ILLEGIBLE:
                        return false;
                    default:
                        switch (m.address) {
                            case INCORRECT:
                                return false;
                            default:
                                System.out.println("Delivering " + m + " normally");
                                return true;
                        }
                }
            }
        },
        RETURN_TO_SENDER {
            boolean handle(Mail m) {
                switch (m.returnAddress) {
                    case MISSING:
                        return false;
                    default:
                        System.out.println("Returning " + m + " to sender");
                        return true;
                }
            }
        };

        abstract boolean handle(Mail m);
    }

    static void handle(Mail m) {
        for(MailHandler handler : MailHandler.values()) {
            if(handler.handle(m)) {
                return;
            }
        }
        System.out.println(m + " is a dead letter");
    }

    public static void main(String[] args) {
        for(Mail mail : Mail.generator(10)) {
            System.out.println(mail.details());
            handle(mail);
            System.out.println("*****");
        }
    }
}

class Mail {
    // “否”會降低隨機選擇的概率:
    enum GeneralDelivery {YES, NO1, NO2, NO3, NO4, NO5}
    enum Scannability {UNSCANNABLE, YES1, YES2, YES3, YES4}
    enum Readability {ILLEGIBLE, YES1, YES2, YES3, YES4}
    enum Address {INCORRECT, OK1, OK2, OK3, OK4, OK5, OK6}
    enum ReturnAddress {MISSING, OK1, OK2, OK3, OK4, OK5}

    GeneralDelivery generalDelivery;
    Scannability scannability;
    Readability readability;
    Address address;
    ReturnAddress returnAddress;
    static long counter = 0;
    long id = counter++;

    public String toString() { return "Mail " + id; }

    public String details() {
        return toString() +
                ", General Delivery: " + generalDelivery +
                ", Address Scanability: " + scannability +
                ", Address Readability: " + readability +
                ", Address Address: " + address +
                ", Return address: " + returnAddress;
    }

    /**
     * 生成測試郵件
     */
    public static Mail randomMail() {
        Mail m = new Mail();
        m.generalDelivery= Enums.random(GeneralDelivery.class);
        m.scannability = Enums.random(Scannability.class);
        m.readability = Enums.random(Readability.class);
        m.address = Enums.random(Address.class);
        m.returnAddress = Enums.random(ReturnAddress.class);
        return m;
    }

    public static Iterable<Mail> generator(final int count) {
        return new Iterable<Mail>() {
            int n = count;
            public Iterator<Mail> iterator() {
                return new Iterator<Mail>() {
                    public boolean hasNext() { return n-- > 0; }
                    public Mail next() { return randomMail(); }
                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }
}
/* Output:
Mail 0, General Delivery: NO2, Address Scanability: UNSCANNABLE, Address Readability: YES3, Address Address: OK1, Return address: OK1
Delivering Mail 0 normally
*****
Mail 1, General Delivery: NO5, Address Scanability: YES3, Address Readability: ILLEGIBLE, Address Address: OK5, Return address: OK1
Delivering Mail 1 automatically
*****
Mail 2, General Delivery: YES, Address Scanability: YES3, Address Readability: YES1, Address Address: OK1, Return address: OK5
Using general delivery for Mail 2
*****
Mail 3, General Delivery: NO4, Address Scanability: YES3, Address Readability: YES1, Address Address: INCORRECT, Return address: OK4
Returning Mail 3 to sender
*****
Mail 4, General Delivery: NO4, Address Scanability: UNSCANNABLE, Address Readability: YES1, Address Address: INCORRECT, Return address: OK2
Returning Mail 4 to sender
*****
Mail 5, General Delivery: NO3, Address Scanability: YES1, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK2
Delivering Mail 5 automatically
*****
Mail 6, General Delivery: YES, Address Scanability: YES4, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK4
Using general delivery for Mail 6
*****
Mail 7, General Delivery: YES, Address Scanability: YES3, Address Readability: YES4, Address Address: OK2, Return address: MISSING
Using general delivery for Mail 7
*****
Mail 8, General Delivery: NO3, Address Scanability: YES1, Address Readability: YES3, Address Address: INCORRECT, Return address: MISSING
Mail 8 is a dead letter
*****
Mail 9, General Delivery: NO1, Address Scanability: UNSCANNABLE, Address Readability: YES2, Address Address: OK1, Return address: OK4
Delivering Mail 9 normally
*****
*///:~

職責鏈由enum MailHandler實現,而enum定義的次序決定了各個解決策略在應用時的次序。對每一封郵件,都要按此順序嘗試每個解決策略,直到其中一個能夠成功地處理該郵件,如果所有的策略都失敗了,那麼該郵件將被判定爲一封死信。

第二十章:註解

註解(也被稱爲元數據)爲我們在代碼中添加信息提供了一種形式化的方法,使我們可以在稍後某個時刻非常方便地使用某些數據。

定義註解

package annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

  除了@符號以外,@Test的定義很像一個空的接口。定義註解時,會需要一些元註解(meta-annotation),如@Target和@Retention。@Target用來定義你的註解將應用於什麼地方(例如是一個方法或者一個域)。@Rectetion用來定義該註解在哪一個級別可用,在源代碼中(SOURCE)、類文件中(CLASS)或者運行時(RUNTIME)。

  沒有元素的註解稱爲標記註解,例如上例種的@Test。

註解元素

  註解元素可用的類型如下:

  • 所有基本類型(int, float, boolean等)
  • String
  • Class
  • enum
  • Annotation
  • 以上類型的數組

  如果你使用了其它類型,編譯器就會報錯。注意,也不允許使用任何包裝類型,不過由於自動打包的存在,這算不上什麼限制。註解也可以作爲元素的類型,也就是說,註解可以嵌套。

元註解

  Java目前只內置了四種元註解,元註解專職負責註解其它的註解:

大多數時候,程序員主要是定義自己的註解,並編寫自己的處理器來處理它們。

  下面是一個簡單的註解,我們可以用它來跟蹤一個項目中的用例。如果一個方法或一組方法實現了某個用例的需求,那麼程序員可以爲此方法加上該註解。於是,項目經理通過計算已經實現的用例,就可以很好地掌控項目的進展。而如果要更新或修改系統的業務邏輯,則維護該項目的開發人員也可以很容易地在代碼中找到對應的用例。

package annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    public int id();
    public String description() default "no description";
}

  注意,id和description類似方法定義。由於編譯器會對id進行類型檢查,因此將用例文檔的追蹤數據庫與源代碼相關聯是可靠的。description元素有一個default值,如果在註解某個方法時沒有給出description的值,則該註解的處理器就會使用此元素的默認值。
  在下面的類中,有三個方法被註解爲用例∶

package annotations;

import java.util.List;

/**
 * @author Mr.Sun
 * @date 2022年09月03日 9:05
 *
 * 註解用例
 */
public class PasswordUtils {

    @UseCase(id = 47, description = "密碼必須至少包含一個數字")
    public boolean validatePassword(String password) {
        return (password.matches("\\w*\\d\\w*"));
    }

    @UseCase(id = 48)
    public String encryptPassword(String password) {
        return new StringBuilder(password).reverse().toString();
    }

    @UseCase(id = 49, description = "新密碼不能等於以前使用的密碼")
    public boolean checkForNewPassword(List<String> prevPasswords, String password) {
        return !prevPasswords.contains(password);
    }
}

  註解的元素在使用時表現爲名一值對的形式,並需要置於@UseCase聲明之後的括號內。在encryptPassword()方法的註解中,並沒有給出description元素的值,因此,在UseCase的註解處理器分析處理這個類時會使用該元素的默認值。
  你應該能夠想象得到如何使用這套工具來“勾勒”出將要建造的系統,然後在建造的過程中逐漸實現系統的各項功能。

編寫註解處理器

  如果沒有用來讀取註解的工具,那註解也不會比註釋更有用。使用註解的過程中,很重要的一個部分就是創建與使用註解處理器。Java SE5擴展了反射機制的API,以幫助程序員構造這類工具。同時,它還提供了一個外部工具apt幫助程序員解析帶有註解的Java源代碼。

  下面是一個非常簡單的註解處理器,我們將用它來讀取PasswordUtils類,並使用反射機制查找@UseCase標記。我們爲其提供了一組id值,然後它會列出在PasswordUtils種找到的用例,以及缺失的用例。

package annotations;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author Mr.Sun
 * @date 2022年09月03日 9:11
 *
 * 編寫註解處理器
 */
public class UseCaseTracker {

    public static void trackUseCases(List<Integer> useCases, Class<?> clazz) {
        for (Method m : clazz.getDeclaredMethods()) {
            UseCase useCase = m.getAnnotation(UseCase.class);
            if (useCase != null) {
                System.out.println("找到@UseCase:" + useCase.id() + " "  + useCase.description());
                useCases.remove(Integer.valueOf(useCase.id()));
            }
        }
        for (Integer i : useCases) {
            System.out.println("警告: 缺失用例:" + i);
        }
    }

    public static void main(String[] args) {
        List<Integer> useCases = new ArrayList<>();
        Collections.addAll(useCases, 47, 48, 49, 50);
        trackUseCases(useCases, PasswordUtils.class);
    }
} /* Output:
找到@UseCase:49 新密碼不能等於以前使用的密碼
找到@UseCase:47 密碼必須至少包含一個數字
找到@UseCase:48 no description
警告: 缺失用例:50
*///:~

  這個程序用到了兩個反射的方法∶getDeclaredMethods()和getAnnotation(),它們都屬於AnnotatedElement接口(Class、Method與Feld等類都實現了該接口)。getAnnoation()方法返回指定類型的註解對象,在這裏就是UseCase。如果被註解的方法上沒有該類型的註解,則返回null值。然後我們通過調用id()和description()方法從返回的UseCase對象中提取元素的值。其中,encriptPassword()方法在註解的時候沒有指定description的值,因此處理器在處理它對應的註解時,通過description()方法取得的是默認值no description。

總結

  枚舉和註解其實在日常開發中都很熟悉,因爲是非常基礎的知識,本文也只是把模糊的概念和比較冷門的知識點記錄下來,方便日後查閱,原來是打算把第二十一章的內容也放在本文的,但發現併發這章內容太多了,限於篇幅,還是打算單獨寫一篇文章進行記錄,而且併發也是比較重要的基礎知識。等把併發這章內容看完了,《Java編程思想》讀書筆記系列也就告一段落了。

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