跳轉到

Java進階:註解(Annotation)的使用與自定義

1. 引言

在Java程式設計中,註解(Annotation)是一種強大而靈活的特性,允許我們為程式碼添加元資料,而不直接影響程式的執行邏輯。註解可以提供編譯時檢查、執行時處理,以及為IDE和其他工具提供資訊,提高程式的可讀性、可維護性和功能性。

本文旨在深入探討Java註解的使用方法和自定義技巧。我們將從基礎概念開始,逐步深入到自定義註解的創建和處理。

2. 註解基礎

什麼是註解

註解(Annotation)是Java 5引入的一種特殊類型的標記,提供一種將元資料(metadata)添加到程式碼中的方法。 註解本身不直接影響程式碼的執行,但可以被編譯器、執行時環境或其他工具讀取和處理。

註解的作用和優點

  1. 提供資訊給編譯器:註解可以用來檢測錯誤或抑制警告。
  2. 編譯時和部署時處理:軟體工具可以處理註解資訊以生成程式碼、XML檔案等。
  3. 執行時處理:某些註解可以在執行時被檢查。

註解的主要優點包括: - 提高程式碼的可讀性和可維護性 - 簡化配置,減少XML配置的使用 - 提供編譯時類型檢查 - 支援自動化處理和程式碼生成

Java內建註解簡介

Java提供一些內建的註解,主要用於提供資訊給編譯器和其他工具。以下是一些常見的內建註解:

  1. @Override:表示子類別方法覆寫父類別方法。
  2. @Deprecated:標記已過時的方法或類別。
  3. @SuppressWarnings:指示編譯器忽略特定的警告。
  4. @FunctionalInterface:表示介面是函數式介面。

3. 常用內建註解

@Override

@Override 註解用於標記一個方法覆寫父類別的方法,可以讓編譯器幫助檢查是否正確覆寫父類別方法,避免因拼寫錯誤或參數不匹配而導致的問題。

範例:

class Parent {
    public void displayMessage() {
        System.out.println("This is a message from the parent class");
    }
}

class Child extends Parent {
    @Override
    public void displayMessage() {
        System.out.println("This is a message from the child class");
    }
}

@Deprecated

@Deprecated 註解用於標記已過時的方法、類別或介面。提醒開發者該元素可能在未來的版本中被移除,應該避免使用。

範例:

public class OldCalculator {
    @Deprecated
    public int add(int a, int b) {
        return a + b;
    }

    public int advancedAdd(int a, int b) {
        // 新的實作方式
        return a + b;
    }
}

@SuppressWarnings

@SuppressWarnings 註解用於抑制特定的編譯器警告。可以應用於類別、方法、變數等多種程式元素。

範例:

public class WarningSuppressExample {
    @SuppressWarnings("unchecked")
    public void useGenerics() {
        List list = new ArrayList();
        list.add("item");
    }
}

@FunctionalInterface

@FunctionalInterface 註解用於標記函數式介面。函數式介面是只包含一個抽象方法的介面,可以用於 Lambda 表達式。

範例:

@FunctionalInterface
public interface Executable {
    void execute();
}

public class MainProgram {
    public static void main(String[] args) {
        Executable task = () -> System.out.println("Executing task");
        task.execute();
    }
}

4. 註解的使用場景

註解在Java開發中有廣泛的應用,可以在不同的階段和場景中發揮作用。以下是一些常見的註解使用場景:

程式碼文件

註解可以用於生成JavaDoc文件,提供API的說明和使用指南。例如,@author、@version、@param、@return等註解可以幫助自動生成清晰、結構化的API文件。

編譯時檢查

某些註解可以在編譯時提供額外的檢查,幫助開發者早期發現潛在問題。例如,@Override確保方法確實覆寫父類別的方法,@FunctionalInterface確保介面只有一個抽象方法。

執行時處理

一些註解可以在程式執行時被讀取和處理,用於改變程式的行為。例如,許多依賴注入框架使用註解來標記需要被注入的依賴項。

框架配置

現代Java框架大量使用註解來簡化配置過程。例如:

  • Spring框架使用@Component、@Autowired等註解進行依賴注入和元件管理。
  • JPA(Java Persistence API)使用@Entity、@Table等註解來映射物件和資料庫表。
  • JUnit測試框架使用@Test、@Before等註解來標記測試方法和設置方法。

範例:

import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

5. 自定義註解

除使用Java內建的註解外,開發者還可以創建自定義註解來滿足特定的需求。自定義註解可以為程式碼添加特定的元資料,這些元資料可以在編譯時或運行時被處理。

註解的宣告語法

自定義註解的宣告類似於介面的宣告,但使用 @interface 關鍵字。基本語法如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AnnotationName {
    String value() default "";
    int count() default 0;
    boolean enabled() default true;
}

元註解的使用

Java提供幾個元註解,用於註解其他註解:

@Retention:指定註解的保留策略

  • RetentionPolicy.SOURCE:僅在源碼中保留
  • RetentionPolicy.CLASS:在類別檔案中保留,但在運行時不可用
  • RetentionPolicy.RUNTIME:在運行時保留,可通過反射讀取

@Target:指定註解可以應用的程式元素類型

  • ElementType.TYPE:類別、介面、列舉
  • ElementType.METHOD:方法
  • ElementType.FIELD:欄位
  • 等等...

  • @Documented:指定該註解應該被包含在JavaDoc中

  • @Inherited:指定該註解可以被子類別繼承

註解參數的定義

自定義註解可以包含參數(也稱為元素)。這些參數的類型可以是基本類型、String、Class、列舉、註解,以及這些類型的陣列。

範例:讓我們創建一個自定義註解來標記需要進行效能測試的方法。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface PerformanceTest {
    int repetitions() default 1;
    String description() default "";
}

class PerformanceTestExample {
    @PerformanceTest(repetitions = 10, description = "Test addition performance")
    public int add(int a, int b) {
        return a + b;
    }
}

6. 註解處理器

註解處理器是用來讀取和處理註解的工具。Java提供兩種主要的註解處理方式:運行時處理和編譯時處理。

反射API與註解

在運行時,我們可以使用Java的反射API來讀取和處理註解,主要用於那些需要在程式執行過程中動態處理的註解。

範例:使用反射API處理我們之前定義的 @效能測試 註解

import java.lang.reflect.Method;

public class AnnotationProcessor {
    public static void processPerformanceTests(Class<?> clazz) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(PerformanceTest.class)) {
                PerformanceTest annotation = method.getAnnotation(PerformanceTest.class);
                System.out.println("Method: " + method.getName());
                System.out.println("Repetitions: " + annotation.repetitions());
                System.out.println("Description: " + annotation.description());
                System.out.println("-----------------------------");
            }
        }
    }

    public static void main(String[] args) {
        processPerformanceTests(PerformanceTestExample.class);
    }
}

運行時註解處理

運行時註解處理允許我們在程式執行過程中讀取和處理註解。這對於實現依賴注入、單元測試框架等功能非常有用。

範例:實現一個簡單的測試運行器

import java.lang.annotation.*;
import java.lang.reflect.Method;

public class SimpleTestRunner {
    public static void runTests(Class<?> testClass) throws Exception {
        Object instance = testClass.getDeclaredConstructor().newInstance();

        for (Method method : testClass.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Test.class)) {
                try {
                    method.invoke(instance);
                    System.out.println(method.getName() + " PASSED");
                } catch (Throwable e) {
                    System.out.println(method.getName() + " FAILED: " + e.getCause());
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        runTests(TestExample.class);
    }
}

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

class TestExample {
    @Test
    public void testAddition() {
        assert 1 + 1 == 2 : "Addition test failed";
    }

    @Test
    public void testSubtraction() {
        assert 3 - 1 == 2 : "Subtraction test failed";
    }

    @Test
    public void testDivision() {
        assert 1 / 0 == 0 : "Division test should throw an exception";
    }
}

編譯時註解處理

編譯時註解處理器在編譯過程中處理註解,可以用來生成額外的原始碼或資源文件,需要實現 javax.annotation.processing.Processor 介面。

範例:一個簡單的編譯時註解處理器框架

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

@SupportedAnnotationTypes("com.example.Generator")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class GeneratorProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Generator.class)) {
            if (element.getKind() != ElementKind.CLASS) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
                    "@Generator can only be applied to classes", element);
                return true;
            }

            String className = ((TypeElement) element).getQualifiedName().toString();
            String generatedClassName = className + "Generated";

            try {
                generateClass(generatedClassName);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
                    "Failed to generate class: " + e.getMessage(), element);
            }
        }
        return true;
    }

    private void generateClass(String className) throws IOException {
        JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(className);
        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
            out.println("package " + className.substring(0, className.lastIndexOf('.')) + ";");
            out.println();
            out.println("public class " + className.substring(className.lastIndexOf('.') + 1) + " {");
            out.println("    public void generatedMethod() {");
            out.println("        System.out.println(\"This is a generated method.\");");
            out.println("    }");
            out.println("}");
        }
    }
}

@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
@java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE)
@interface Generator {}

7. 實際應用案例

註解在現代Java開發中有廣泛的應用。讓我們來看看一些實際的應用案例,這些案例展示註解如何在不同的框架和場景中發揮作用。

單元測試中的註解使用

JUnit是Java中最流行的單元測試框架之一,大量使用註解來簡化測試的編寫。

範例:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {
    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void testAddition() {
        assertEquals(4, calculator.add(2, 2), "2 + 2 should equal 4");
    }

    @Test
    void testSubtraction() {
        assertEquals(2, calculator.subtract(4, 2), "4 - 2 should equal 2");
    }

    @Test
    void testMultiplication() {
        assertEquals(6, calculator.multiply(2, 3), "2 * 3 should equal 6");
    }

    @Test
    @Disabled("Division functionality not yet implemented")
    void testDivision() {
        // To be implemented
    }

    @Test
    void testDivisionByZero() {
        assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0),
                "Division by zero should throw ArithmeticException");
    }

    @AfterEach
    void tearDown() {
        calculator = null;
    }
}

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Cannot divide by zero");
        }
        return a / b;
    }
}

依賴注入框架中的註解

Spring框架廣泛使用註解來實現依賴注入和元件管理。

範例:

import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public User getUser(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
    }

    @Transactional
    public User createUser(User user) {
        return userRepository.save(user);
    }

    @Transactional
    public User updateUser(Long id, User userDetails) {
        User user = getUser(id);
        user.setName(userDetails.getName());
        user.setEmail(userDetails.getEmail());
        return userRepository.save(user);
    }

    @Transactional
    public void deleteUser(Long id) {
        User user = getUser(id);
        userRepository.delete(user);
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Spring Data JPA will implement this interface
}

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    // Getters and setters
}

ORM框架中的註解

JPA(Java Persistence API)使用註解來映射Java物件和資料庫表。

範例:

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "order_number", unique = true, nullable = false)
    private String orderNumber;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    @Column(name = "order_date", nullable = false)
    private LocalDateTime orderDate;

    @Column(name = "total_amount", nullable = false)
    private Double totalAmount;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private OrderStatus status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();

    @Version
    private Long version;

    // Constructors, getters, and setters

    public void addOrderItem(OrderItem item) {
        orderItems.add(item);
        item.setOrder(this);
    }

    public void removeOrderItem(OrderItem item) {
        orderItems.remove(item);
        item.setOrder(null);
    }

    // Other business methods
}

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "email", unique = true, nullable = false)
    private String email;

    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();

    // Constructors, getters, and setters
}

@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @Column(name = "product_name", nullable = false)
    private String productName;

    @Column(name = "quantity", nullable = false)
    private Integer quantity;

    @Column(name = "price", nullable = false)
    private Double price;

    // Constructors, getters, and setters
}

public enum OrderStatus {
    PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}

8. 實踐與注意事項

在使用Java註解時,可以嘗試遵循以下建議和注意事項:

適度使用註解

雖然註解可以簡化程式碼並提高可讀性,但過度使用可能會適得其反。請記住以下幾點:

  1. 只在真正需要的地方使用註解。
  2. 避免使用註解來替代良好的程式設計。
  3. 權衡註解和其他配置方法(如XML)的優缺點。

註解的命名規範

遵循良好的命名規範可以提高程式碼的可讀性和一致性:

  1. 使用駝峰命名法,以大寫字母開頭。
  2. 選擇清晰、描述性的名稱。
  3. 避免使用縮寫,除非們是眾所周知的。

例如:@JsonSerialize, @Transactional, @Autowired

註解文件的重要性

為自定義註解提供清晰的文件是非常重要的:

  1. 使用JavaDoc來描述註解的用途、參數和使用方法。
  2. 提供使用範例。
  3. 說明註解的保留策略和目標元素。

例如:

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

/**
 * Marks a method for performance testing.
 * 
 * This annotation is used to indicate that a method should be subject to
 * performance testing. It allows specifying the number of times the method
 * should be executed during the test and a description of the test.
 * 
 * <p>Example usage:
 * <pre>
 * {@code
 * @PerformanceTest(repetitions = 1000, description = "Test database query performance")
 * public void testDatabaseQuery() {
 *     // Method implementation
 * }
 * }
 * </pre>
 * 
 * @since 1.0
 * @author YourName
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PerformanceTest {
    /**
     * Specifies the number of times the annotated method should be executed
     * during the performance test.
     * 
     * @return the number of repetitions for the test
     */
    int repetitions() default 1;

    /**
     * Provides a description of the performance test.
     * 
     * This can be used to explain the purpose of the test or to provide
     * additional context.
     * 
     * @return the description of the test
     */
    String description() default "";

    /**
     * Specifies whether to ignore this test in certain environments.
     * 
     * This can be useful for skipping long-running tests in development environments.
     * 
     * @return true if the test should be ignored in certain environments, false otherwise
     */
    boolean ignoreInDev() default false;
}

注意事項

  1. 確保註解處理器的效能:特別是在編譯時處理大量註解時,效能可能會成為一個問題。
  2. 考慮向後兼容性:在修改或移除已有的註解時,要考慮對現有程式碼的影響。
  3. 避免循環依賴:在使用依賴注入等功能時,要小心避免創建循環依賴。
  4. 測試註解:確保為使用註解的程式碼編寫充分的測試,包括正面和負面測試案例。

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI