Java Annotations, Reflection & Design Patterns
Use reflection to inspect classes at runtime. Learn built-in annotations, custom metadata, and common structural design patterns.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What are annotations in Java and what are some commonly used built-in annotations?
Annotations are metadata attached to code (classes, methods, fields) that don't directly affect program logic but provide information used by the compiler, tools, or at runtime via reflection.
| Annotation | Purpose |
|---|---|
| @Override | Verifies a method actually overrides a parent method (compile-time check) |
| @Deprecated | Marks code as outdated, generates compiler warnings if used |
| @SuppressWarnings | Tells the compiler to ignore specific warnings |
| @FunctionalInterface | Enforces an interface has exactly one abstract method |
| @SafeVarargs | Suppresses unchecked warnings for varargs with generics |
class Animal {
public void makeSound() {
System.out.println('Some sound');
}
}
class Dog extends Animal {
@Override // catches typos - if method name doesn't match parent, compile error
public void makeSound() {
System.out.println('Bark');
}
}
class Utility {
@Deprecated
public static void oldMethod() {
System.out.println('This method is outdated');
}
@SuppressWarnings('unchecked')
public static void suppressExample() {
List rawList = new ArrayList();
List<String> list = rawList; // would normally warn, suppressed here
}
}
// Using a deprecated method triggers a compiler warning
Utility.oldMethod(); // warning: [deprecation] oldMethod() is deprecated
// @Override catching a real bug
class BadDog extends Animal {
// @Override
// public void makeSond() { // typo! Without @Override, this silently
// System.out.println('Bark'); // creates a NEW method instead of overriding
// }
}
Q2. How do you create a custom annotation in Java?
Custom annotations are defined using @interface, combined with meta-annotations (@Retention, @Target) that control where the annotation can be used and how long it's retained.
import java.lang.annotation.*;
// Defining a custom annotation
@Retention(RetentionPolicy.RUNTIME) // available at runtime via reflection
@Target(ElementType.METHOD) // can only be applied to methods
public @interface Testable {
String author() default 'Unknown'; // element with a default value
int priority() default 1;
String[] tags() default {};
}
// Using the custom annotation
class Calculator {
@Testable(author = 'John', priority = 5, tags = {'math', 'critical'})
public int add(int a, int b) {
return a + b;
}
@Testable // uses default values
public int subtract(int a, int b) {
return a - b;
}
}
// Reading the annotation via reflection at runtime
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
Method method = Calculator.class.getMethod('add', int.class, int.class);
if (method.isAnnotationPresent(Testable.class)) {
Testable annotation = method.getAnnotation(Testable.class);
System.out.println('Author: ' + annotation.author()); // John
System.out.println('Priority: ' + annotation.priority()); // 5
}
}
}
// RetentionPolicy options:
// SOURCE - discarded by the compiler, not in bytecode (e.g. @Override)
// CLASS - kept in bytecode but not available at runtime (default)
// RUNTIME - available via reflection at runtime (needed for frameworks)
// ElementType options: TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, etc.
Q3. What is the Reflection API in Java and what can you do with it?
Reflection lets a program inspect and manipulate classes, methods, fields, and constructors at RUNTIME, even without knowing their names at compile time - the foundation of many frameworks (Spring, Hibernate, JUnit).
import java.lang.reflect.*;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void greet() {
System.out.println('Hello, ' + name);
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName('Person'); // or Person.class
// Inspecting fields
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println('Field: ' + field.getName() + ' - ' + field.getType());
}
// Inspecting methods
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println('Method: ' + method.getName());
}
// Creating an instance dynamically
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object person = constructor.newInstance('John', 30);
// Accessing a PRIVATE field via reflection
Field nameField = clazz.getDeclaredField('name');
nameField.setAccessible(true); // bypasses access control
String name = (String) nameField.get(person);
System.out.println('Name: ' + name); // John
// Calling a PRIVATE method via reflection
Method greetMethod = clazz.getDeclaredMethod('greet');
greetMethod.setAccessible(true);
greetMethod.invoke(person); // Hello, John
}
}
// Real-world uses: Spring's dependency injection, JUnit finding @Test methods,
// JSON libraries (Gson/Jackson) mapping fields, ORM frameworks like Hibernate
Q4. What is the Singleton design pattern and how do you implement it thread-safely in Java?
Singleton ensures a class has exactly ONE instance and provides a global access point to it - commonly used for configuration managers, logging, and connection pools.
// UNSAFE in multi-threaded environments - two threads could both
// pass the null check simultaneously and create two instances
class UnsafeSingleton {
private static UnsafeSingleton instance;
private UnsafeSingleton() {}
public static UnsafeSingleton getInstance() {
if (instance == null) {
instance = new UnsafeSingleton(); // race condition possible
}
return instance;
}
}
// Thread-safe with synchronized (simple but has performance overhead
// on every call, even after the instance already exists)
class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
// Double-checked locking - synchronizes only during first creation
class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance; // volatile is essential here
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) { // first check, no locking (fast path)
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) { // second check, inside the lock
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
// BEST APPROACH - Bill Pugh Singleton using a static holder class
// (thread-safe by the JVM's class-loading guarantees, no explicit locking needed)
class BillPughSingleton {
private BillPughSingleton() {}
private static class Holder {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return Holder.INSTANCE; // Holder loaded lazily, only on first access
}
}
// ALTERNATIVE - Enum singleton (simplest, inherently thread-safe and serialization-safe)
enum EnumSingleton {
INSTANCE;
public void doSomething() { System.out.println('Working'); }
}
EnumSingleton.INSTANCE.doSomething();
Q5. What is the Factory design pattern and when should you use it?
The Factory pattern encapsulates object creation logic in a dedicated method or class, so client code depends on an interface/abstract type rather than concrete classes - useful when object creation involves conditional logic or varies by input.
// Common interface for products
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() { System.out.println('Drawing Circle'); }
}
class Square implements Shape {
public void draw() { System.out.println('Drawing Square'); }
}
class Rectangle implements Shape {
public void draw() { System.out.println('Drawing Rectangle'); }
}
// Factory - centralizes creation logic
class ShapeFactory {
public static Shape createShape(String type) {
switch (type.toLowerCase()) {
case 'circle': return new Circle();
case 'square': return new Square();
case 'rectangle': return new Rectangle();
default: throw new IllegalArgumentException('Unknown shape: ' + type);
}
}
}
// Client code doesn't need to know concrete classes
Shape shape1 = ShapeFactory.createShape('circle');
shape1.draw(); // Drawing Circle
Shape shape2 = ShapeFactory.createShape('square');
shape2.draw(); // Drawing Square
// Benefits:
// - Adding a new shape type only requires changing the Factory,
// not every place that creates shapes
// - Client code depends on the Shape interface, not concrete classes
// (Dependency Inversion Principle)
// Real-world Java examples of the Factory pattern:
// Calendar.getInstance(), NumberFormat.getInstance(),
// java.sql.DriverManager.getConnection() (Abstract Factory variant)
Q6. What is the Builder design pattern and how does it help with complex object construction?
The Builder pattern constructs complex objects step by step, avoiding constructors with many parameters (telescoping constructors) and making object creation more readable.
// PROBLEM - telescoping constructor anti-pattern
class PizzaOld {
PizzaOld(String size, boolean cheese, boolean pepperoni, boolean mushroom, boolean olives) {
// hard to remember parameter order, easy to make mistakes
}
}
new PizzaOld('Large', true, false, true, false); // what do these booleans mean?!
// SOLUTION - Builder pattern
class Pizza {
private final String size; // required
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushroom;
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushroom = builder.mushroom;
}
public static class Builder {
private final String size; // required, set in constructor
private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushroom = false;
public Builder(String size) {
this.size = size;
}
public Builder cheese(boolean value) { this.cheese = value; return this; }
public Builder pepperoni(boolean value) { this.pepperoni = value; return this; }
public Builder mushroom(boolean value) { this.mushroom = value; return this; }
public Pizza build() {
return new Pizza(this);
}
}
}
// Readable, self-documenting object creation with method chaining
Pizza pizza = new Pizza.Builder('Large')
.cheese(true)
.mushroom(true)
.build();
// Only sets what's needed, order doesn't matter, clear what each value means
// Real-world Java examples: StringBuilder, StringBuffer,
// java.time.LocalDate (via factory+builder-like chains),
// Stream.Builder, Lombok's @Builder annotation
Q7. What is the Observer design pattern and where is it used in Java?
The Observer pattern defines a one-to-many dependency where multiple 'observer' objects are notified automatically whenever a 'subject' object's state changes - foundational for event-driven systems.
import java.util.*;
// Observer interface - defines the notification contract
interface Observer {
void update(String event);
}
// Subject - maintains a list of observers and notifies them
class NewsPublisher {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer observer) {
observers.add(observer);
}
public void unsubscribe(Observer observer) {
observers.remove(observer);
}
public void publishNews(String news) {
for (Observer observer : observers) {
observer.update(news); // notify ALL subscribers
}
}
}
// Concrete observers
class EmailSubscriber implements Observer {
private String email;
public EmailSubscriber(String email) { this.email = email; }
public void update(String event) {
System.out.println('Emailing ' + email + ': ' + event);
}
}
class SmsSubscriber implements Observer {
public void update(String event) {
System.out.println('SMS: ' + event);
}
}
NewsPublisher publisher = new NewsPublisher();
publisher.subscribe(new EmailSubscriber('john@example.com'));
publisher.subscribe(new SmsSubscriber());
publisher.publishNews('Breaking News: Java 21 released!');
// Both subscribers are notified automatically
// Real-world uses of the Observer pattern in Java:
// - Java Swing/AWT event listeners (ActionListener, MouseListener)
// - java.util.Observer/Observable (deprecated since Java 9)
// - Spring's ApplicationEvent and ApplicationListener
// - Reactive programming libraries (RxJava) build heavily on this concept
Q8. What is dependency injection and how does it relate to design patterns?
Dependency injection (DI) is a technique where an object's dependencies are provided from outside rather than created internally, achieving loose coupling and following the Dependency Inversion Principle.
// WITHOUT dependency injection - tight coupling
class EmailService {
public void send(String message) {
System.out.println('Email: ' + message);
}
}
class NotificationServiceOld {
private EmailService emailService = new EmailService(); // hardcoded, tightly coupled
public void notify(String message) {
emailService.send(message);
}
}
// Problem: NotificationServiceOld can ONLY ever use EmailService,
// hard to test (can't substitute a mock), hard to swap implementations
// WITH dependency injection - loosely coupled via an interface
interface MessageService {
void send(String message);
}
class EmailServiceImpl implements MessageService {
public void send(String message) {
System.out.println('Email: ' + message);
}
}
class SmsServiceImpl implements MessageService {
public void send(String message) {
System.out.println('SMS: ' + message);
}
}
class NotificationService {
private final MessageService messageService;
// Constructor injection - dependency provided from OUTSIDE
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.send(message);
}
}
// Client decides WHICH implementation to inject
NotificationService emailNotifier = new NotificationService(new EmailServiceImpl());
NotificationService smsNotifier = new NotificationService(new SmsServiceImpl());
emailNotifier.notify('Hello'); // Email: Hello
smsNotifier.notify('Hello'); // SMS: Hello
// Easy to test with a mock implementation
class MockMessageService implements MessageService {
public void send(String message) {
System.out.println('Mock sent: ' + message); // no real email/SMS sent in tests
}
}
// Frameworks like Spring automate this wiring using @Autowired,
// managing object creation and injection so you rarely write 'new' manually
Q9. What is the difference between the Comparable pattern and the Strategy design pattern?
Comparable defines a class's single natural ordering baked into the class itself, while the Strategy pattern lets you define and SWAP entire algorithms (behaviors) at runtime, independent of the class using them.
// Comparable - one fixed, natural ordering built into the class
class Employee implements Comparable<Employee> {
String name;
double salary;
Employee(String name, double salary) { this.name = name; this.salary = salary; }
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary); // fixed: always sorts by salary
}
}
// Strategy pattern - interchangeable algorithms via an interface
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println('Paid ' + amount + ' via Credit Card');
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println('Paid ' + amount + ' via PayPal');
}
}
class ShoppingCart {
private PaymentStrategy paymentStrategy;
// Strategy can be swapped at RUNTIME, unlike Comparable's fixed ordering
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
paymentStrategy.pay(amount);
}
}
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100); // Paid 100 via Credit Card
cart.setPaymentStrategy(new PayPalPayment()); // swap the algorithm entirely
cart.checkout(50); // Paid 50 via PayPal
// Comparator is actually a real-world application of the Strategy pattern -
// it lets you swap the 'sorting algorithm/criteria' without touching the class:
List<Employee> employees = new ArrayList<>();
employees.sort(Comparator.comparing(e -> e.name)); // one strategy
employees.sort(Comparator.comparingDouble(e -> e.salary)); // a different strategy
Java Annotations, Reflection & Design Patterns
Use reflection to inspect classes at runtime. Learn built-in annotations, custom metadata, and common structural design patterns.
What are annotations in Java and what are some commonly used built-in annotations?
Annotations are metadata attached to code (classes, methods, fields) that don't directly affect program logic...
How do you create a custom annotation in Java?
Custom annotations are defined using @interface, combined with meta-annotations (@Retention, @Target) that con...
What is the Reflection API in Java and what can you do with it?
Reflection lets a program inspect and manipulate classes, methods, fields, and constructors at RUNTIME, even w...
What is the Singleton design pattern and how do you implement it thread-safely in Java?
Singleton ensures a class has exactly ONE instance and provides a global access point to it - commonly used fo...
What is the Factory design pattern and when should you use it?
The Factory pattern encapsulates object creation logic in a dedicated method or class, so client code depends...
What is the Builder design pattern and how does it help with complex object construction?
The Builder pattern constructs complex objects step by step, avoiding constructors with many parameters (teles...
What is the Observer design pattern and where is it used in Java?
The Observer pattern defines a one-to-many dependency where multiple 'observer' objects are notified automatic...
What is dependency injection and how does it relate to design patterns?
Dependency injection (DI) is a technique where an object's dependencies are provided from outside rather than...
What is the difference between the Comparable pattern and the Strategy design pattern?
Comparable defines a class's single natural ordering baked into the class itself, while the Strategy pattern l...