深入探討 Android、Java 註解應用:分析註解與 Enum 的差異 | Android APT

深入探討 Android、Java 註解應用:分析註解與 Enum 的差異 | Android APT

Overview of Content

在現代 Java、Android 開發中,註解(Annotation)和 Enum 是兩個非常重要的工具… 本篇文章將全面介紹這兩者的應用與差異,幫助讀者更好地理解如何在實際項目中有效地運用這些技術

我們將從 Android 中最基本的註解開始,帶領讀者了解各種常見註解的作用與用法,進一步探討 Nullness 註解資源類型註解執行緒註解RGB 註解註解範圍許可權註解、重新定義函數註解及回傳值註解,這些註解能夠在多方面提升代碼的穩定性、安全性及可讀性。

此外,我們還將深入比較 Enum 和 Annotation 在 Android 開發中的應用,討論它們的優勢和劣勢;包括 Enum 的反編譯解析,以及與 Android IntDef 的比較,幫助開發者在不同場景下做出最佳選擇。

接著,我們將介紹 Java 中的標準註解,包含編譯相關註解、資源註解、自定義註解、元註解等,並解析自定義註解的屬性設置,讓開發者掌握更多的註解應用技巧,提升代碼質量和靈活性。

最後,我們會討論註解提取技術,講解如何在不同的保留策略下進行註解的提取和處理,並詳細介紹 SOURCE 註解在 Android APT 技術中的應用,幫助開發者更好地利用註解處理工具

Java 註解從 Java 1.5 添加到 Java,以下會使用 Android 註解 & Java 註解

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


Android 基礎註解/概述

Support Annotation Library 從 19.1 引進全新的函數套件,在開發中加入程式可提升程式的品質 (可讓編譯器檢查),增加程式碼的健壯

● Android 中自身有的註解介紹


// Android gradle 加入註解的依賴
dependencies {
    implementation 'com.android.support:support-annotations:28.0.0'
}

Nullness 註解

● Nullness 註解是在提醒使用該函數的使用者,傳入的參數是否可以為 Null,或是不可以為 Null,這對於提高程式碼的質量非常有幫助

註解名意義
@Nullable參數 or 回傳值可以為空
@NonNull參數 or 回傳值不可以為空

public class TestAnnotation {

    public static void main(String... args) {
        String s1 = useAnnotation("Alien");
        String s = useAnnotation(null);
    }

    private static @NonNull String useAnnotation(@NonNull String str) {
        return "Hello World, " + str;
    }

}

--實作--

資源類型註解

● 資源在 Android 中通常以整數來代表 (存在於 R.java 中),該註解可以避免資源被放置到錯的參數中

註解名意義
@XXXResXXX 代表為 android.R.XXX 類型的資源,有 Animator, Anim, String, Array, Bool, Color, Drawable, Integer, Layout, ID...+Res

public class TestAnnotation {

    public static void main(String... args) {
        TestRxxx(new Integer[]{});
        TestRxxx(new String[]{});
        TestRxxx(R.id.response);
    }

    private static void TestRxxx(@IdRes int id) {

    }
}

--實作--

執行緒註解

註解名意義
@UiThread一個應用而言可能存在多個 UiThread,每個 UI 執行緒對應不同視窗
@MainThread標記執行在主線程,一個應用只有一個主執行緒 (就是 @UiThread 執行緒),大部分情況 @MainThread 使用在生命週期相關函數
@WorkThread背景後台工作的執行緒
@BinderThread標記執行在 Binder 的執行緒

@UiThread & @MainThread 一般來說是可以互換的,因為 UI Thread 通常就是指 Main Thrad(並非一定)

像是 Android 的輕量級異步執行緒框架 AnsyncTask 就有此用這些

… 有關於 AnsyncTask 解釋請點擊連結了解(Template 設計模式 | 實現與解說 | Android source AsyncTask 微框架


// AnsyncTask.java

    @WorkerThread
    protected abstract Result doInBackground(Params... params);

    ...
    @MainThread
    protected void onPreExecute() {
    }

    ...
    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onPostExecute(Result result) {
    }

    ...
    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onProgressUpdate(Progress... values) {
    }

    ...
    @SuppressWarnings({"UnusedParameters"})
    @MainThread
    protected void onCancelled(Result result) {
        onCancelled();
    }

RGB 註解

● RGB / ARGB 每個字母佔 8Byte

● 它跟 @ColorRes 的差別在 @ColorRes 只接收 Resouce 的資源項目,@ColorInt 接收所有符合的資源內容

註解名意義
@ColorInt需要傳入 RGB 顏色整數

--實作--

只要符合 @ColorInt 規範的整數就可以 (以下可以看出 ID 類的標明也是符合,但整數 20 不可以)

註解範圍

註解名意義Example
@Size(min=x)集合下限@Size(min=1)
@Size(max=x)集合上限@Size(max=3)
@Size(value=x)固定集合數量@Size(value=2)
@Size(multiple=x)集合大小可為 x 的倍數數量@Size(multiple=2)
@IntRange(x)參數類型可為 int or long@IntRange(from=0,to=255)
@FloatRange(x)參數類型可為 float or double@FloatRange(from=-1.0,to=1.0)

public class TestAnnotation {

    public static void main(String... args) {
        TestSizeMin(new int[]{1,2});
        TestSizeMin(new int[]{});

        TestSizeMax(new int[]{1,2});
        TestSizeMax(new int[]{1,2,3,4});

        TestSizeFixed(new int[]{1,2});
        TestSizeFixed(new int[]{1,2,3});

        TestSizeMultiple(new int[]{1,2});
        TestSizeMultiple(new int[]{1,2,3,4,5});

        TestIntRange(123);
        TestIntRange(256);

        TestFloatRange(1.0F);
        TestFloatRange(-2.0F);
    }

    private static void TestSizeMin(@Size(min = 1) int[] params) {
    }

    private static void TestSizeMax(@Size(max = 3) int[] params) {
    }

    private static void TestSizeFixed(@Size(value = 2) int[] params) {
    }

    private static void TestSizeMultiple(@Size(multiple = 2) int[] params) {
    }
    
    private static void TestIntRange(@IntRange(from = 0, to = 255) long a) {
    }
    
    private static void TestFloatRange(@FloatRange(from = -1.0, to = 1.0) float a) {
    }
}

● 限定於陣列才可判斷,其餘不可判斷


public class TestAnnotation {

    public static void main(String... args) {

        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        TestSizeInList(list);

        TestSizeOther(1,3,5,6,7,8);

    }

    private static void TestSizeInList(@Size(3) ArrayList<Integer> list) {
    }

    private static void TestSizeOther(@Size(3) int... params) {
    }
}

--實作--

許可權註解

註解名意義
@RequiresPermission()提醒使用者要在 AndroidManifest.mxl 中宣告許可權
@RequiresPermission(allof={x,y})以下的權限皆需要
@RequiresPermission(anyOf={x,y})最少需要其中一個

重新定義函數註解

● Android 的生命週期 onCreate 就有使用;建構函數不需要 @CallSuper

註解名意義
@CallSuper提醒使用者要複寫該方法

public class TestAnnotation {

    public static void main(String... args) {
        new RichMan(new PoorMan()).wear();
    }
}

abstract class Decorator {
    Decorator() {
    }

    public abstract void wear();
}

class PoorMan extends Decorator {
    @Override
    public void wear() {
        System.out.println("UnderPant");
    }
}

abstract class WorkMan extends Decorator {
    protected Decorator d;

    protected WorkMan(Decorator d) {
        this.d = d;
    }

    @CallSuper
    @Override
    public void wear() {
        d.wear();
    }
}

class RichMan extends WorkMan {
    protected RichMan(Decorator d) {
        super(d);
    }

    @Override
    public void wear() {
        super.wear();
        System.out.println("Jacket");
        System.out.println("Pant");
        System.out.println("T-shirt");
        System.out.println("sock");
        System.out.println("shoes");
    }
}

以 裝飾器模式來說每個實作類都需要呼叫該父類的方法,但使用者有時會忘記呼叫 super() 這時就可以使用,用來提醒使用框架者要記得呼叫父類方法

這種提醒機制並「非強制」,就算使用者不遵從提醒仍可編譯過

--實作--

回傳值註解

● 如果某函數需要對 return 值作處理這時就可以呼叫 @CheckResult,把要寫的警訊寫在 suggest 中,Android 第三方 Lib 很常使用

註解名意義
@CheckResult(suggest="x")提醒使用者要注意回傳值

Enum 與 Annotation 差異

我們在撰寫程式時,常常使用 Enum 來表達出可使用的選項(同時限制使用者只能使用 Enum 中的選項);而使用註解也可以達到告訴使用者該參數能接收的選項

同樣注意到的是,這並非是一個硬式限制,更像是一種提醒

Enum 反編譯解析

Enum 就算是一個「類」,而一個類所占用的標頭最少是 12 個字節(byte),所以會耗費更多的記憶體空間


class MyTestClass {
    enum Level {
        LEVEL_1,
        LEVEL_2,
        LEVEL_3,
        LEVEL_4,
        LEVEL_5,
        LEVEL_6,
    }
}

上面程式碼經過 ASM 反組譯後會變為,每一個成員皆為一個 enum 類,所 佔用空間過大 (Byte 描述 Class 會耗費多一點空間)

Enum vs. Android IntDef

@IntDef 註解由 Androidx 提供,並標明 @Target({ANNOTATION_TYPE}) 表示它是註解在註解上的註解

InDef 可使用在 IDE 檢查,其階段只保留到 Source,以下是它的源碼

Android 還有 StringDef... 等等可以使用


// 源碼

@Retention(SOURCE)                // 保存到 Source 階段
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    /** Defines the allowed constants for this element */
    int[] value() default {};

    boolean flag() default false;

    boolean open() default false;
}

@IntDef 特性有

A. 效能更高

在 Android 平台上,使用 enum 會比使用 int 消耗更多的內存,因為 enum 會引入額外的方法和屬性;使用 @IntDef 則不會有這個問題,因為它本質上還是使用整數值

B. 編譯期檢查

@IntDef 提供了編譯期檢查,確保只能使用指定的整數值,避免了使用其他不合法的值

C. 提高可讀性

通過 @IntDef 註解,可以使代碼更加易讀,因為我們可以定義一組有意義的常量來表示不同的狀態或類型

@IntDef 的使用方式

要使用 IntDef 需要自定義一個註解,並且標註該註解作用的區域 @Target,還有它的保留階段 @Retention


class MyTestClass {

    //"1. "
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @IntDef(value = {LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5})
    @Retention(RetentionPolicy.SOURCE)
    @interface MyLevel {

    }

    public static final int LEVEL_1 = 1;
    public static final int LEVEL_2 = 2;
    public static final int LEVEL_3 = 3;
    public static final int LEVEL_4 = 4;
    public static final int LEVEL_5 = 5;


    public static void main(String []args){
        System.out.println("Hello World");

        setLevel(1111);    //"2. "
        setLevel(3);
        setLevel(LEVEL_3);
    }

    public static void setLevel(@MyLevel int level) {
        System.out.println("level: " + level);
    }
}

A. 使用自定義註解,要標示它作用的區域,如果沒有標示 ElementType.PARAMETER 該註解則不能作用於函式參數

B. 放入非標準的格式編譯仍然可以過,但是寫程式時跳出警告訊息,讓程式更加健壯

可看出所占用的空間變少,一個標示只佔 4 個字節


Java 標準註解

● Java 也可使用,不限定 Android,它定義在 java.langjava.lang.annotationjavax.annotation 套件中

編譯相關註解:SafeVarargs 使用

註解名意義
@Override覆寫父類方法,如果並無覆寫則會報錯誤
@Deprecated遺棄的方法,編譯器會警告使用者
@SuppressWarnings(value={x,y})抑制編譯器警告,可抑制多個警告
@SafeVarargs支援資料可變長度
@Generated一般用於給程式自動產生代碼,不建議手動修改
@FuntionalInterface介面只有一個方法,可使用在 Lambda

註解 @SafeVargs 目的 & 意義

其主要目的是處理 泛型 的可變長參數(T...t) ,告編譯器此泛型是安全長參數

可變長參數使用數組儲存,而數組 & 泛型不能很好的混合使用

數組 & 泛型 ? 可以使用泛型數組不會有警告

數組:編譯期間就已確定(靜態確認)

泛型:運行時才能確定數據,因此編譯時無法判別其正確性,導致產生警告(動態確認)… 這時我們就可以使用 @SafeVargs 去壓制警告

註解 @SafeVargs 只能宣告在 static or final 的函數

● 對於 SafeVarargsSuppressWarnings 使用範例如下


public class TestProblemOfGeneric {

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();

        //"2. "
        FixList.add2List(list1, "Pan", "Kyle", "Alien");
        FixList.add2List(list2, "One", "Two", "Three");

        List<List<String>> list3 = new ArrayList<>();
        //"3-1. "
        FixList.add2List(list3, list1, list2);
        //"3-2. "
        FixList.TestMethod(Arrays.asList("Hello!"), 
                           Arrays.asList("World!"));
    }

}

class FixList {
    public static <T> void add2List(List<T> list, T...data) {
        for(T t : data) {
            list.add(t);
        }
    }

    //"1. "
    public static void TestMethod(List<String>...list) {
        Object[] o = list;
        //"4. "
        o[0] = Arrays.asList(new Integer(123));
        String str = list[0].get(0);
        System.out.println("Test: " + str);
    }
}

A. ... 代表可變的參數長度,編譯器會將其變成 數組,也就是說 List<String>...list 可以看做 List<String>[]

B. 一般類型可正常轉為可變長度,再轉為數組

C. Java 不允許泛型類類型的數組 (無法確認數據類型,會發出警告),使用 @SafeVarargs 使其安全使用 (確定不會錯誤時再添加這個註解)

Object[] o = list 這個語句會轉化為 Object[] o = (List[]) list; 這是正確的

Arrays.asList(new Integer(123)),就類型錯誤會拋出 ClassCastException 異常

Arrays.asList 方法返回的是泛型數組

D. 如果沒有 @SafeVarargs 註解壓住錯誤的話,IDE 會警告發生 HashPollution 錯誤(想了結詳情情點擊連結)

Java 資源註解

● 一般用於 Java EE (Java EnterPrise),Android 較少使用

註解名意義
@PostConstruct用在控制物件生命週期的環境中,在呼叫建構函數後應立即被呼叫
@PreDestory同上,在銷毀物件之前應被呼叫的方法
@Resoure用於 Web 容器的資源植入,單一資源
@Resoures同上,表示陣列資源

Java 自定義註解

● 透過 @interface 關鍵字自定義註解,其形式與接口類似,不過多了 @ 符號,可當作是標籤,可在每個類、參數、回傳值上貼上標籤


// Sample 1
@interface HelloWorld {

}

@HelloWorld
class MyAnnotation {

}

// Sample 2 in Android
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
    @IdRes int id();
}

@MyAnnotation(id = R.id.sample_text)
TextView tv;

Java 元註解

● 可理解為 註解的註解,不能修飾一般方法,可應用在其他註解上,分為五種…

A. @Retention (註解保留期間)(Java 1.5 加入)

選項在 RetentionPolicy 中 (emum 類)

元註解名保留期間使用
RetentionPolicy.SOURCE註解只留在源碼中,編譯過後即捨棄 (保存到 .java 為止)IDE、APT
RetentionPolicy.CLASS註解留到編譯進行時,並不會加載到 JVM 中 (保存到 .class 為止)AOT 思想,插件化
RetentionPolicy.RUNTIME註解留到 runtime,會加載到 JVM 中,所以反射可以使用 (保存在 JVM 中)反射

根據元註解 (Retention) 不同有不同效果

● Source 編譯器可 使用註解偵錯、警告,APT

● Class 編譯階段:可使用 字節碼插入,但並不會保留到運行期間(也就是你可以在 .class 階段看到它的存在,但運行時就會消失)

● Runtime 註解:可在 Runtime 時提取 反射,這時的註解 也會代碼的一部分,它會保留到運行期間

Runtime 註解通常使用在框架中

如果是開發 Android 或其他專案時使用到 Runtime 反射機制,要特別注意「混淆機制」,混淆會導致反射運作不正常! 要注意去寫混肴文件,跳過混淆

B. @Target (目標)註解所修飾的對象範圍(Java 1.5 加入)

選項在 ElementType 中 (emum 類),import java.lang.annotation.RetentionPolicy;

元素適用
ElementType.ANNOTATION_TYPE註解型態宣告(給註解貼上標籤)
ElementType.CONSTRUCTOR註解建構函數
ElementType.FIELD註解屬性
ElementType.LOCAL_VARIABLE註解區域變數
ElementType.METHOD註解方法
ElementType.PACKAGE註解包
ElementType.PARAMETER註解方法參數
ElementType.TYPE類別 or 介面
ElementType.USE類型的用途

可註解多個,使用 {} 包覆數值,範例如下…


@Target({
    ElementType.ANNOTATION_TYPE, 
    ElementType.CONSTRUCTOR, 
    ElementType.TYPE}
)

C. @Documented(Java 1.5 加入)

將註解中的元素包含入 JavaDoc 中

D. @Inherited (繼承)(Java 1.5 加入)

該註解可以被繼承 (不是說註解被繼承),如果父類使用過註解,子類沒有使用,則該子類就會繼承該父類註解

E. @Repeatable (重複)(Java 1.8 加入)

該註解可多次使用

自定義註解的屬性

● 在我們自定義註解時,內部 只可以有屬性不能有方法

而註解中的 value 是一個特殊的屬性,value 不需要指定


public @interface MyAnnnotation {
    // value 接收 String 類型參數
    String value();    

    // StrArray 接收 string array 類型
    String[] StrArray();  // 需要指定
}

● 自定義註解中的屬性包含基礎的 8 個類還有 3 個特殊類,分別是…

● 基礎類型:byte, short, int, long, float, double, char, boolean

● 特殊類型:Classenuminterface


//"1. " 使用預設質、或指定
@MyAnnnotation(id=10, name="ABC", getEnum = MyEnum.THREE)
class ToDo {
}

interface MyInterface {
}

enum MyEnum {
    ONE, TWO, THREE
}

@interface MyAnnnotation {
    int id() default -1; 
    String name() default "";

    //"2. " 不能設定方法
    //void setAddress(String addr);		 

    Class<?> get() default Todo.class;
    MyEnum getEnum() default MyEnum.ONE;

    enum TestEnum {
        ONE, TWO, THREE
    }

    interface Test {

    }
}

註解提取

Android 中最常使用到的就是 APT 註解解析器,解析使用者所定義的註解

RUNTIME

● RUNTIME 反射提取,可以猜考 Java 反射篇,像是 Gson 源碼就有使用到 (包括泛型反射)

CLASS

● 用於插件化,AOT 思想,字節碼插入程式的技術,而要插入的點可以透過註解打上標記(這裡有機會我再補充文章,其思想概念是在編譯時插入 ByteCode)

SOURCE:Android APT 使用

APT 是作用在 Source 過程中,Java 在編譯 java 檔案前會先儲存註解,而處理該註解的則稱為註解處理器

graph LR java_f(.java file) --> |執行編譯| 處理註解 subgraph 編譯 class_f(.class file) 處理註解 --> 拋棄註解 --> |產出| class_f end

A. 建立一個 MyAnnotation 的註解,並用元注解標註 SOURCEFIELD


package com.oo.jnidemo;    // 之後會使用到

...

@Retention(RetentionPolicy.SOURCE)    // 只保存在源碼階段
@Target(ElementType.FIELD)
public @interface MyAnnotation {
    @IdRes int id();
}

B. 新建 Java or Kotlin Library (File > New > New Module...),並取名為 annotations

這個 Module 作用是 註解處理器在 processor Module 的 build.gradle 添加依賴 (implementation)


apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotations')
}

sourceCompatibility = "7"
targetCompatibility = "7"

在該 Library 下創建一個類,取名為 ClassProcessor,繼承 AbstractProcessor,並覆寫以下表格的方法 (目前為了測試是在 source 所以只使用 process 函數)

名稱功能參數功能
init將會被註解處理工具調用ProcessingEnvironment 提供工具類
process每個處理器的主函數RoundEnvironment 可以查詢特定註解的被註解元素
getSupportedAnnotationTypes必須指定的方法,==指定該註解處理器是註冊給哪個註解==,也可以使用註解 @SupportedAnnotationTypes(==全類名==)Non
getSupportedSourceVersionJava 版本Non

//"a. "
@SupportedAnnotationTypes("com.oo.jnidemo.MyAnnotation")
public class MyAnnotationProcess extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //"b. "
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "!!!======== Annotation Processor ========!!!");

        return false;
    }
}

getSupportedAnnotationTypes 返回的是一個字符串的集合,它包含本處理器將要處理的註解類型的 合法全名,可使用 Class 方法的 getCanonicalName

● 輸出到 console 視窗,因為創建時是使用 Java Lib,所以不能使用 Log,要使用 Message,並使用 Diagnostic.Kind.NOTE

C. 註冊「註解處理器」就像是 Activity 要在 AndroidManifest;註解處理器也要註冊,有兩種方法 手動 & 自動

以下說明「Google 提供的自動註冊註解 Library」的使用,Google 開源 AutoService (可能版本間容不好,要小心使用,但速度較快)


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotations')
    // new
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2'
}

在 module processor's gradle 下新增,annotationProcessor 依賴

在註解處理器下新增 @AutoService(Process.class) 的註解


// 自動註冊時,Processor.class 這是固定寫法 
@AutoService(Processor.class)

// 代表了該註解處理器會處理這個註解 (包名 + 全類名)
@SupportedAnnotationTypes("xxx.yyy.zzz")

// 指定 JDK 編譯版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)

// 接收的參數
@SupportedOptions({"xxx", "yyy"})

public class ClassProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ...
    }
}

D. 使用註解:使用的主工程項目中,添加要使用的 project 依賴(這個 project 就是我們在上面寫的註解處理器),之後主工程在編譯時,指定的註解就會被分析


dependencies {
    ...

    implementation project(':annotations')  // 註解
    annotationProcessor project(':processor')    // 註解處理器
}

在主項目中使用以上提供的註解(@MyAnnotation


public class MainActivity extends AppCompatActivity {

    @MyAnnotation(id = R.id.sample_text)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

最後 Rebuild project,在 Build 視窗可以看到 Task **:app:compileDebugJavaWithJavac,Javac 在編譯 .java 文件,它在處理註解

--結果--


更多的 Java 語言相關文章

Java 語言深入

● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識

深入 Java 物件導向

● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響