Adnotacje w Javie – jak działają

TL;DR: Adnotacje to metadata dodawane do kodu Java które nie wpływają bezpośrednio na wykonanie, ale mogą być odczytywane przez kompilator, IDE lub w runtime przez reflection. Podstawowe to @Override, @Deprecated, @SuppressWarnings. Custom annotations pozwalają na tworzenie własnych frameworków.

Dlaczego adnotacje zmieniły sposób programowania w Javie?

Przed Java 5 konfiguracja była w XML-ach, a metadata w komentarzach. Adnotacje to jak etykiety na produktach w sklepie – dają dodatkowe informacje bez zmiany zawartości. Spring, Hibernate i JUnit używają annotations zamiast XML configuration, co czyni kod bardziej readable i maintainable.

Adnotacje zostały wprowadzone w Java 5 (2004) jako part of JSR 175. Framework Spring od wersji 2.5 (2007) mocno wykorzystuje annotation-based configuration.

Co się nauczysz:

  • Czym są adnotacje i jak działają w JVM
  • Built-in annotations: @Override, @Deprecated, @SuppressWarnings
  • Retention policies: SOURCE, CLASS, RUNTIME
  • Tworzenie custom annotations
  • Odczytywanie annotations przez reflection
Wymagania wstępne: Podstawy Java (klasy, metody), znajomość inheritance i polymorphism, podstawowe pojęcie reflection będzie pomocne.

Czym są adnotacje w Javie?

Adnotacje to forma metadata – dane o danych. Pozwalają na dodanie informacji do kodu bez wpływu na jego wykonanie. Kompilator, IDE, lub aplikacja w runtime może używać tych informacji do różnych celów.

Annotation – specjalny rodzaj interface w Javie, który może zawierać elementy (methods bez implementacji) używane do przechowywania metadata.
Adnotacje to jak naklejki na pudełkach w magazynie – nie zmieniają zawartości, ale dają informacje o tym co jest w środku, gdzie należy dostarczyć, czy jest kruche, etc.

Podstawowa składnia

// Najprostsza adnotacja - marker annotation
@Override
public void toString() {
    return "Example";
}

// Adnotacja z single value
@SuppressWarnings("unchecked")
public List getRawList() {
    return new ArrayList();
}

// Adnotacja z wieloma parametrami
@RequestMapping(value = "/users", method = RequestMethod.GET)
public String getUsers() {
    return "users";
}

// Adnotacja z array values
@Test(expected = {IllegalArgumentException.class, NullPointerException.class})
public void testExceptions() {
    // test code
}

Built-in adnotacje Java

@Override – sprawdzanie nadpisywania

public class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

public class Dog extends Animal {
    
    @Override
    public void makeSound() {  // Kompilator sprawdzi czy metoda istnieje w parent
        System.out.println("Woof!");
    }
    
    // @Override
    // public void makeSond() {  // BŁĄD KOMPILACJI - typo w nazwie metody
    //     System.out.println("Woof!");
    // }
}
Pro tip: Zawsze używaj @Override gdy nadpisujesz metodę. IDE i kompilator złapią błędy typu refactoring parent class, typos w nazwie metody, czy wrong method signature.

@Deprecated – oznaczanie przestarzałych elementów

public class Calculator {
    
    /**
     * @deprecated Użyj {@link #calculateAdvanced(int, int)} zamiast tej metody.
     * Ta metoda będzie usunięta w wersji 2.0.
     */
    @Deprecated
    public int calculate(int a, int b) {
        return a + b;  // Stara, prosta implementacja
    }
    
    public int calculateAdvanced(int a, int b) {
        // Nowa, lepsza implementacja z walidacją
        if (a < 0 || b < 0) {
            throw new IllegalArgumentException("Negative numbers not allowed");
        }
        return a + b;
    }
}

// Użycie - IDE pokaże warning
Calculator calc = new Calculator();
int result = calc.calculate(5, 3);  // IDE: "calculate() is deprecated"

@SuppressWarnings - wyłączanie warningów

public class WarningExamples {
    
    @SuppressWarnings("unchecked")  // Wyłącz unchecked warnings
    public List getLegacyList() {
        List rawList = getLegacyRawList();  // Raw type warning suppressed
        return (List) rawList;      // Unchecked cast warning suppressed
    }
    
    @SuppressWarnings({"unused", "deprecation"})  // Wiele warningów
    public void legacyCode() {
        String unusedVariable = "This won't show unused warning";
        Calculator calc = new Calculator();
        calc.calculate(1, 2);  // Deprecated method warning suppressed
    }
    
    // Popularne typy warningów:
    // "all" - wszystkie warnings
    // "unchecked" - unchecked operations
    // "unused" - unused variables/methods
    // "deprecation" - deprecated API usage
    // "rawtypes" - raw types
}
Pułapka: Używaj @SuppressWarnings ostrożnie i only dla specific cases. Nie ukrywaj prawdziwych problemów - lepiej napraw underlying issue.

Retention Policies - gdzie żyją adnotacje

@Retention - określa lifecycle adnotacji

Retention PolicyOpisPrzykład użycia
SOURCETylko w source code, usuwane przez kompilator@Override, @SuppressWarnings
CLASSW .class files, ale nie w runtimeAnnotation processors, bytecode manipulation
RUNTIMEDostępne w runtime przez reflection@Test, @Autowired, @RequestMapping
import java.lang.annotation.*;

// SOURCE retention - tylko dla kompilator/IDE
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface SourceOnly {
    String value();
}

// CLASS retention - dla annotation processors
@Retention(RetentionPolicy.CLASS)  // Default retention
@Target(ElementType.TYPE)
public @interface ClassLevel {
    String name();
}

// RUNTIME retention - dla reflection w aplikacji
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RuntimeAvailable {
    String description();
    int priority() default 0;
}

Tworzenie custom annotations

Przykład: @Benchmark annotation

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)  // Musi być RUNTIME dla reflection
@Target(ElementType.METHOD)          // Tylko na metodach
@Documented                          // Include w JavaDoc
public @interface Benchmark {
    
    String description() default "";   // Optional parameter z default value
    
    int iterations() default 1;         // Ile razy wykonać benchmark
    
    TimeUnit unit() default TimeUnit.MILLISECONDS;  // Enum jako parameter
    
    boolean warmup() default true;      // Boolean parameter
    
    String[] tags() default {};         // Array parameter
}

// Użycie annotation
public class PerformanceTest {
    
    @Benchmark(description = "String concatenation test", iterations = 1000)
    public String testStringConcat() {
        String result = "";
        for (int i = 0; i < 100; i++) {
            result += "test" + i;
        }
        return result;
    }
    
    @Benchmark(iterations = 500, unit = TimeUnit.MICROSECONDS, tags = {"io", "critical"})
    public void testFileRead() throws IOException {
        Files.readAllLines(Paths.get("test.txt"));
    }
}

@Target - gdzie można używać annotation

// Różne cele dla annotations
@Target(ElementType.TYPE)           // Klasy, interfaces, enums
@Target(ElementType.METHOD)         // Metody
@Target(ElementType.FIELD)          // Pola
@Target(ElementType.PARAMETER)      // Parametry metod
@Target(ElementType.CONSTRUCTOR)    // Konstruktory
@Target(ElementType.PACKAGE)        // Pakiety (package-info.java)

// Można kombinować cele
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MultiTarget {
    String value();
}

// Użycie na klasie
@MultiTarget("This is a class")
public class Example {
    
    // Użycie na metodzie
    @MultiTarget("This is a method")
    public void doSomething() {
        // implementation
    }
}

Odczytywanie annotations przez reflection

Benchmark processor - praktyczny przykład

import java.lang.reflect.Method;

public class BenchmarkProcessor {
    
    public void processBenchmarks(Object testInstance) {
        Class clazz = testInstance.getClass();
        
        // Znajdź wszystkie metody z @Benchmark
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Benchmark.class)) {
                
                // Odczytaj annotation
                Benchmark benchmark = method.getAnnotation(Benchmark.class);
                
                System.out.println("Running benchmark: " + method.getName());
                System.out.println("Description: " + benchmark.description());
                System.out.println("Iterations: " + benchmark.iterations());
                
                // Wykonaj benchmark
                runBenchmark(testInstance, method, benchmark);
            }
        }
    }
    
    private void runBenchmark(Object instance, Method method, Benchmark benchmark) {
        try {
            int iterations = benchmark.iterations();
            boolean warmup = benchmark.warmup();
            
            // Warmup round
            if (warmup) {
                System.out.println("Warmup...");
                method.invoke(instance);
            }
            
            // Actual benchmark
            long startTime = System.nanoTime();
            
            for (int i = 0; i < iterations; i++) {
                method.invoke(instance);
            }
            
            long endTime = System.nanoTime();
            long duration = endTime - startTime;
            
            // Convert to specified time unit
            TimeUnit unit = benchmark.unit();
            long convertedDuration = convertTime(duration, unit);
            
            System.out.printf("Benchmark completed: %d %s for %d iterations%n", 
                            convertedDuration, unit.name().toLowerCase(), iterations);
            
            // Process tags
            String[] tags = benchmark.tags();
            if (tags.length > 0) {
                System.out.println("Tags: " + String.join(", ", tags));
            }
            
        } catch (Exception e) {
            System.err.println("Benchmark failed: " + e.getMessage());
        }
    }
    
    private long convertTime(long nanos, TimeUnit unit) {
        switch (unit) {
            case NANOSECONDS: return nanos;
            case MICROSECONDS: return nanos / 1000;
            case MILLISECONDS: return nanos / 1_000_000;
            case SECONDS: return nanos / 1_000_000_000;
            default: return nanos;
        }
    }
}

// Użycie benchmark processor
public class BenchmarkExample {
    public static void main(String[] args) {
        PerformanceTest test = new PerformanceTest();
        BenchmarkProcessor processor = new BenchmarkProcessor();
        
        processor.processBenchmarks(test);
    }
}

Sprawdzanie wszystkich annotations na klasie

public class AnnotationInspector {
    
    public void inspectClass(Class clazz) {
        System.out.println("Inspecting class: " + clazz.getSimpleName());
        
        // Annotations na klasie
        Annotation[] classAnnotations = clazz.getAnnotations();
        System.out.println("Class annotations: " + classAnnotations.length);
        
        for (Annotation annotation : classAnnotations) {
            System.out.println("  - " + annotation.annotationType().getSimpleName());
        }
        
        // Annotations na metodach
        for (Method method : clazz.getDeclaredMethods()) {
            Annotation[] methodAnnotations = method.getAnnotations();
            if (methodAnnotations.length > 0) {
                System.out.println("Method " + method.getName() + " annotations:");
                for (Annotation annotation : methodAnnotations) {
                    System.out.println("  - " + annotation.annotationType().getSimpleName());
                }
            }
        }
    }
}
Czy adnotacje wpływają na wydajność aplikacji?

SOURCE i CLASS annotations nie wpływają na runtime performance. RUNTIME annotations mają minimalny overhead - są ładowane z classfile, ale reflection calls to read them mogą być kosztowne jeśli używane często.

Kiedy używać custom annotations?

Gdy chcesz stworzyć framework, dodać metadata do code (jak @Test w JUnit), lub zastąpić XML configuration. Custom annotations są świetne dla aspect-oriented programming i dependency injection.

Czy mogę dodać annotation do annotation?

Tak! To nazywa się meta-annotations. @Retention, @Target, @Documented są meta-annotations. Możesz tworzyć composed annotations jak @RestController (która łączy @Controller i @ResponseBody).

Co to jest annotation processor?

Tool który runs podczas compilation i może generate code based na annotations. Lombok używa annotation processors do generowania getters/setters. To bardziej zaawansowany topic niż reflection.

Czy annotations mogą dziedziczyć?

Nie bezpośrednio, ale możesz użyć @Inherited meta-annotation żeby annotation była inherited przez subclasses. Default behavior to brak inheritance.

🚀 Zadanie dla Ciebie

Stwórz własny mini-framework do testowania:

  1. @Test annotation z parameters: timeout, expected exception
  2. @BeforeEach i @AfterEach annotations dla setup/cleanup
  3. TestRunner który używa reflection do uruchamiania testów
  4. Report generation - ile testów passed/failed
  5. Bonus: @Disabled annotation do pomijania testów

Test framework na simple Calculator class z metodami add/subtract/divide. Include edge cases jak division by zero.

Przydatne zasoby:

Których adnotacji używasz najczęściej w swoim kodzie? Czy tworzyłeś już własne custom annotations? Do czego je wykorzystujesz?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry