Contents

Class<?> is the entry point to all reflection. Every loaded class has exactly one Class instance, obtainable via a .class literal on a known type, getClass() on an instance, or Class.forName() with a fully-qualified name (useful when the class name is only known at runtime). From a Class object you can enumerate constructors, methods, fields, annotations, implemented interfaces, and supertype information.

import java.lang.reflect.*; // Every class has a Class<T> object — the entry point to reflection // Three ways to get a Class object Class<String> c1 = String.class; // class literal Class<?> c2 = "hello".getClass(); // from instance Class<?> c3 = Class.forName("java.lang.String"); // by fully-qualified name (may throw) System.out.println(c1 == c2); // true — same Class object // Class metadata Class<?> cls = ArrayList.class; System.out.println(cls.getName()); // java.util.ArrayList System.out.println(cls.getSimpleName()); // ArrayList System.out.println(cls.getPackageName()); // java.util System.out.println(cls.getSuperclass()); // class java.util.AbstractList System.out.println(cls.isInterface()); // false System.out.println(cls.isEnum()); // false System.out.println(cls.isRecord()); // false (Java 16+) // Implemented interfaces for (Class<?> iface : cls.getInterfaces()) { System.out.println(iface.getSimpleName()); // List, RandomAccess, Cloneable, Serializable } // Modifiers int mod = cls.getModifiers(); System.out.println(Modifier.isPublic(mod)); // true System.out.println(Modifier.isAbstract(mod)); // false // Checking if one class is assignable from another System.out.println(List.class.isAssignableFrom(ArrayList.class)); // true System.out.println(ArrayList.class.isInstance(new ArrayList<>())); // true

getDeclaredField() locates any field declared in the class regardless of access modifier; calling setAccessible(true) on it suppresses the private access check so you can read and write private fields. In contrast, getField() only returns public fields, including those inherited from superclasses. Once you have a Field object, Field.get(instance) reads the value and Field.set(instance, value) writes it.

class Person { public String name; private int age; private static int count = 0; } Class<?> cls = Person.class; // getFields() — public fields (including inherited) // getDeclaredFields() — all fields (including private), but NOT inherited Field[] allFields = cls.getDeclaredFields(); for (Field f : allFields) { System.out.printf("%-10s %-10s %s%n", Modifier.toString(f.getModifiers()), f.getType().getSimpleName(), f.getName()); } // public String name // private int age // private int count (static) // Access a specific field by name Field ageField = cls.getDeclaredField("age"); ageField.setAccessible(true); // bypass private access restriction Person p = new Person(); p.name = "Alice"; // p.age is private — set via reflection: ageField.set(p, 30); System.out.println(ageField.get(p)); // 30 // Read a private field Field nameField = cls.getDeclaredField("name"); nameField.setAccessible(true); System.out.println(nameField.get(p)); // Alice // Get field type information System.out.println(ageField.getType()); // int System.out.println(ageField.getGenericType()); // int setAccessible(true) bypasses Java's access controls. In Java 9+, the module system adds additional restrictions — accessing fields in non-exported packages from unnamed modules may throw InaccessibleObjectException. The JVM may also emit warnings about illegal reflective access.

getDeclaredMethod() locates any method by name and parameter types, including private ones; follow it with setAccessible(true) to bypass access control. Method.invoke(instance, args...) calls the method — pass null as the first argument for static methods. Any exception thrown by the invoked method is wrapped in an InvocationTargetException; always unwrap it with getCause() to retrieve the original exception.

class Calculator { public int add(int a, int b) { return a + b; } private int multiply(int a, int b) { return a * b; } public static int square(int n) { return n * n; } } Class<?> cls = Calculator.class; Calculator calc = new Calculator(); // Get and invoke a public method Method addMethod = cls.getMethod("add", int.class, int.class); int result = (int) addMethod.invoke(calc, 3, 4); System.out.println(result); // 7 // Get and invoke a private method Method mulMethod = cls.getDeclaredMethod("multiply", int.class, int.class); mulMethod.setAccessible(true); int product = (int) mulMethod.invoke(calc, 5, 6); System.out.println(product); // 30 // Invoke a static method — pass null as the instance Method squareMethod = cls.getMethod("square", int.class); int sq = (int) squareMethod.invoke(null, 9); System.out.println(sq); // 81 // getMethods() — all public methods including inherited (from Object etc.) // getDeclaredMethods() — all methods declared in this class, any access, not inherited for (Method m : cls.getDeclaredMethods()) { System.out.println(m.getName() + " ← " + Arrays.stream(m.getParameterTypes()).map(Class::getSimpleName).toList()); } // Generic return type Method m = List.class.getMethod("get", int.class); System.out.println(m.getReturnType()); // class java.lang.Object System.out.println(m.getGenericReturnType()); // E (type variable) // Exception handling in invoke try { addMethod.invoke(calc, "wrong", "types"); // passes wrong types } catch (InvocationTargetException e) { System.err.println("Wrapped exception: " + e.getCause()); // the actual exception } catch (IllegalArgumentException e) { System.err.println("Wrong argument types"); }

getDeclaredConstructor(parameterTypes...) locates a constructor by its exact parameter types; passing no arguments finds the no-arg constructor. Calling newInstance() on the returned Constructor object is preferred over the deprecated Class.newInstance(), which could not handle constructors that declare checked exceptions and silently propagated them. Always use Constructor.newInstance() for reliable error handling.

// newInstance() (deprecated Java 9+) vs getDeclaredConstructor().newInstance() class Config { private final String host; private final int port; public Config() { this("localhost", 8080); } public Config(String host, int port) { this.host = host; this.port = port; } @Override public String toString() { return host + ":" + port; } } // Using no-arg constructor Constructor<Config> noArg = Config.class.getDeclaredConstructor(); Config c1 = noArg.newInstance(); System.out.println(c1); // localhost:8080 // Using parameterized constructor Constructor<Config> withArgs = Config.class.getDeclaredConstructor(String.class, int.class); Config c2 = withArgs.newInstance("prod.example.com", 443); System.out.println(c2); // prod.example.com:443 // Instantiate by class name — factory pattern String className = "com.example.PluginImpl"; Class<?> pluginClass = Class.forName(className); Object plugin = pluginClass.getDeclaredConstructor().newInstance(); // Cast to a known interface: // MyPlugin plugin = (MyPlugin) pluginClass.getDeclaredConstructor().newInstance(); // List all constructors for (Constructor<?> con : Config.class.getDeclaredConstructors()) { System.out.println(con.getParameterCount() + " params: " + Arrays.stream(con.getParameterTypes()).map(Class::getSimpleName).toList()); }

Reflection is the foundation of major Java frameworks: Spring uses it for dependency injection and bean wiring, JUnit for test method discovery, and Jackson for JSON serialization and deserialization. The downsides are significant: reflection is 10–100x slower than direct calls due to argument boxing and access checks, it bypasses compile-time type safety so errors surface only at runtime, and it breaks encapsulation by exposing private internals. Use it for framework and infrastructure code, and prefer interfaces, lambdas, or MethodHandles wherever possible in application logic.

// USE CASE 1: Simple bean-to-map converter (like JSON serializers do) Map<String, Object> toMap(Object bean) throws Exception { Map<String, Object> map = new LinkedHashMap<>(); for (Field f : bean.getClass().getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; f.setAccessible(true); map.put(f.getName(), f.get(bean)); } return map; } // usage: toMap(new Person("Alice", 30)) → {name=Alice, age=30} // USE CASE 2: JUnit-style test runner — run all methods annotated with @Test void runTests(Class<?> testClass) throws Exception { Object instance = testClass.getDeclaredConstructor().newInstance(); for (Method m : testClass.getDeclaredMethods()) { if (m.isAnnotationPresent(Test.class)) { // @Test annotation check System.out.println("Running: " + m.getName()); m.invoke(instance); } } } // LIMITATIONS and pitfalls: // 1. Performance — reflection is 10–100x slower than direct calls (invoke overhead) // Solution: cache Method/Field objects; use MethodHandle for hot paths // 2. No compile-time safety — typos in field/method names → runtime exceptions // Solution: use constants or annotation processors // 3. Module system (Java 9+) — modules can restrict reflective access // Solution: open packages in module-info.java: // opens com.example.model to com.framework; // 4. Breaking encapsulation — avoid in application code; use for frameworks/tools only // Java 7+ MethodHandle — faster than reflection for repeated calls MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(String.class, "toUpperCase", MethodType.methodType(String.class)); String upper = (String) mh.invoke("hello"); // "HELLO" — faster than Method.invoke() Reserve reflection for framework and infrastructure code. Application business logic should use direct method calls. For type-safe, high-performance alternatives to reflection, explore MethodHandle (Java 7+) and compile-time code generation with annotation processors.