AlgoMaster Logo

Inspecting Classes

Medium Priority24 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Once a Class object is available, the next question is what it can answer. The reflection API exposes a rich vocabulary for reading a type's structure: its fields, its methods, its constructors, the modifiers on each, the annotations attached, and the relationships to its superclass and interfaces. This lesson covers only the read-only side of that vocabulary.

The model is simple. Every member a class can have (field, method, constructor) has a matching reflection object (Field, Method, Constructor) that describes it. Asking a Class for these objects is the way to walk the structure. This lesson builds a small describe() utility that takes any class and prints what it contains, using a small e-commerce class hierarchy (Product, Order, Cart, Customer) as the running example.

The Inspection Vocabulary

The Class object is the entry point. From there, three families of accessors expose members.

The diagram shows the four kinds of question a Class answers. Fields, methods, and constructors each come in two flavors: a "public, including inherited" version and a "declared, including private but only on this class" version. The next two sections unpack that distinction. The metadata family covers modifiers, parent class, interfaces, and category questions like "is this an enum?"

The Product class used for most of the lesson:

This is the test subject. It has one public static field, three private fields, one package-private field, two constructors, four getters, one boolean method, and an overridden toString. Reflection should be able to surface every one of these.

getFields() vs getDeclaredFields()

One source of confusion in the reflection API is the difference between getX() and getDeclaredX(). The naming makes them sound like minor variants. They are not.

MethodWhat it returns
getFields()Public fields only, but including those inherited from superclasses and interfaces
getDeclaredFields()All fields declared on this class, regardless of visibility, but NOT inherited ones

The same pattern holds for methods and constructors. Both axes matter: visibility (public vs all) and origin (this class vs the whole hierarchy).

getFields() returns only TAX_RATE because it is the only public field on the class. The three private fields and the package-private one are hidden from it. getDeclaredFields() returns all five fields that the Product source file declares.

Adding a subclass shows the inherited-vs-declared dimension.

getFields() picked up both DOWNLOAD_FEE (declared here) and TAX_RATE (inherited from Product), because both are public. It did not pick up licenseKey because it is private. getDeclaredFields() listed DOWNLOAD_FEE and licenseKey because those are the two fields declared on DigitalProduct, but it did not list TAX_RATE because that field belongs to Product.

The cross product as a table:

QuestionMethod to call
"Everything accessible through this class, regardless of declaration site"getFields()
"Everything declared right here, including private members"getDeclaredFields()
"All fields, public and private, from anywhere in the hierarchy"Walk getDeclaredFields() up the chain using getSuperclass()
"All public fields declared here, ignoring inheritance"Filter getDeclaredFields() by Modifier.isPublic

There is no single built-in method for the third row. The loop has to be written by hand, as covered later in the lesson.

Methods and Constructors Follow the Same Rule

The same pair of accessors applies to methods and constructors. The naming and meaning match the field case.

The exact order of getMethods() is not guaranteed, so runs may show methods in a slightly different order. The contents are guaranteed: getMethods() lists every public method visible through Product, including the ones inherited from Object (wait, equals, hashCode, getClass, notify, notifyAll, and the three overloads of wait). getDeclaredMethods() returns only the six methods the Product source file declares.

getMethods() and getDeclaredMethods() walk the class structure on each call and allocate a fresh array. When inspecting the same class repeatedly in a hot path, hold a reference to the Method instead of looking it up every iteration.

Constructors work the same way. Product has two public constructors, so both come back from either call.

Constructors do not get inherited in Java, so the two methods return the same list as long as every constructor is public. Adding a private constructor (for a singleton or a factory) makes getConstructors() hide it while getDeclaredConstructors() still shows it.

Looking Up a Specific Member by Name

Sometimes only one field or method is needed, not all of them. The lookup methods take the name (and for methods and constructors, the parameter types) and return the matching reflection object.

MethodReturnsIf not found
getField(String)The public field with that name (including inherited)NoSuchFieldException
getDeclaredField(String)The declared field with that name on this classNoSuchFieldException
getMethod(String, Class<?>...)The public method with that name and parameter typesNoSuchMethodException
getDeclaredMethod(String, Class<?>...)The declared method on this classNoSuchMethodException
getConstructor(Class<?>...)The public constructor matching the parameter typesNoSuchMethodException
getDeclaredConstructor(Class<?>...)The declared constructor on this classNoSuchMethodException

The parameter-types argument is a varargs of Class<?>. For getMethod("setPrice", double.class), pass double.class, not Double.class, because the method's parameter is the primitive. Mixing those up is a frequent cause of NoSuchMethodException.

If the name does not exist or the parameter types do not match, a checked exception is thrown. The exception is descriptive: it names exactly what was being looked up.

Both NoSuchFieldException and NoSuchMethodException are checked, so call sites must declare them or catch them. This is one of the reasons reflection code looks heavier than direct code: every lookup forces an exception decision.

One common mistake to flag early: getMethod("getPrice", double.class) will throw NoSuchMethodException even though getPrice exists, because that method takes zero arguments and the lookup asks for one of type double. The lookup matches on signature, not just on name. To find a no-arg method, pass no parameter types at all: getMethod("getPrice").

Reading Field Metadata

A Field object exposes everything the source file declared about that field: its name, its type, its generic type, and its modifiers. This section is read-only.

Modifier.toString(int) converts the raw modifier bits into a readable string in the order the Java Language Specification prefers (visibility first, then static, then final, and so on). The getType() call returns the field's declared type as a Class. Calling getSimpleName() strips the package, so the result is String instead of java.lang.String.

For generic fields, getType() returns the erased type. getGenericType() returns the full generic type, which can be a ParameterizedType. Here is the difference.

getType() is appropriate when only the raw class of the field is needed. getGenericType() is appropriate when the type parameter matters: for example, a serializer that needs to know "is this a List<Product> or a List<Order>?" Both methods are cheap and read-only.

The Modifier class also exposes static predicates: Modifier.isPublic(int), Modifier.isPrivate(int), Modifier.isStatic(int), Modifier.isFinal(int), Modifier.isVolatile(int), Modifier.isTransient(int), and so on. Use them for a single yes/no answer instead of parsing a string.

This pattern is the basis of any inspector that needs to filter by visibility or by "is this a constant?" (static plus final). The describe() utility at the end of the lesson uses it.

Reading Method Metadata

A Method object exposes a richer set of properties than a Field because methods have parameters, a return type, and a declared list of thrown exceptions.

AccessorWhat it returns
getName()The method name, like "getPrice"
getReturnType()The erased return type as Class<?>
getGenericReturnType()The full generic return type, including type parameters
getParameterTypes()An array of Class<?> describing the parameter types
getParameterCount()The number of parameters
getParameters()An array of Parameter objects with name and type information
getExceptionTypes()The declared checked exception types
getModifiers()The raw modifier bits, same as for fields

The next program walks every declared method on Product and prints a one-line summary.

Each method is printed in the order it appears in the class file, though the JVM does not guarantee source order. The same logic works for any class, which is the property the describe() utility exploits.

When a method has parameters, getParameters() returns a Parameter[]. Each Parameter carries a name and a type. By default, the JVM does not store parameter names in the class file, so the names come back as arg0, arg1, and so on. Compiling with the -parameters flag (javac -parameters or the Maven compiler plugin's parameters option) preserves the real names and getName() returns them. Frameworks like Spring rely on this for binding request parameters.

Output (compiled without -parameters):

Output (compiled with -parameters):

Parameter name reflection is a build-time switch. When code or a framework needs real parameter names, the whole module must be compiled with -parameters, not just the calling code.

Generic return types follow the same pattern as generic field types.

For methods that declare checked exceptions in their throws clause, getExceptionTypes() returns the list. The common use is a serializer or RPC framework that needs to know which exceptions can flow out of a method.

Reading Constructor Metadata

Constructor is the third reflection object and behaves like Method minus the return type. It exposes parameter types, parameter count, exception types, modifiers, and so on. A more realistic Order class illustrates it.

getConstructors() returns only the two public constructors. getDeclaredConstructors() adds the private one. This is a common pattern in factory-method classes: a public set of constructors for callers, a private one for internal use, and only the declared accessor sees both.

Class-Level Metadata

The Class object also describes itself: its modifiers, its parent class, its interfaces, its package, and whether it is something special like an interface, enum, annotation, or record.

Output (assuming `Product` is in the default package):

Three details about this list:

getSuperclass() returns Object for any class that does not extend something else. For interfaces, it returns null. For arrays, it returns Object. A loop over it climbs the full inheritance chain.

getInterfaces() returns only the interfaces directly named in the implements clause. It does not include interfaces inherited transitively through a superclass or through another interface. The full set requires walking both getInterfaces() and getSuperclass().getInterfaces() recursively.

The is* family is the cheap way to ask category questions. isInterface() is true for an interface declaration. isEnum() is true for an enum (and the enum's class shows extends java.lang.Enum<...> as its superclass). isAnnotation() is true for an annotation declaration. isRecord() was added in Java 16 and is true for record types.

Records get their own dedicated accessor as well. A record's components are not the same as its fields from the JVM's point of view: the language guarantees that a record component is a one-to-one match with a private final field of the same name and type, but the reflection API has a separate RecordComponent type for them.

getRecordComponents() returns null on non-record classes, not an empty array, so always check isRecord() first when calling it on arbitrary classes.

Reading Annotations on Classes, Fields, Methods, and Parameters

The reflection side of annotations is small: every annotated element exposes the same four-method vocabulary.

MethodReturns
getAnnotation(Class<A> type)The annotation of that type, or null if not present
isAnnotationPresent(Class<? extends Annotation> type)true if the annotation is present
getAnnotations()All annotations present, including those inherited via @Inherited
getDeclaredAnnotations()All annotations declared directly on this element, ignoring @Inherited

The methods exist on Class, Field, Method, Constructor, and Parameter. They behave the same way everywhere. Only annotations whose retention is RUNTIME are visible to reflection; the others were dropped before the class reached the JVM.

A small example using @Deprecated, which has runtime retention, on each of those elements:

The parameter loop printed arg0 instead of reason because this example was compiled without -parameters. The parameter name reflection rule from earlier applies here too.

isAnnotationPresent is a convenience method for checking presence without reading the data inside the annotation.

To read the data inside the annotation, use getAnnotation and call the element accessors on the returned annotation object. Deprecated has two elements added in Java 9: since and forRemoval.

The since and forRemoval calls are real methods on the Deprecated interface. The annotation reflection returns an object that implements the annotation's interface, and element methods are called on it directly.

The difference between getAnnotations() and getDeclaredAnnotations() only shows up for classes, and only for annotations that are themselves marked @Inherited. By default annotations are not inherited, so the two methods return the same array. @Inherited is a meta-annotation that opts in to inheritance; the takeaway is that for fields, methods, constructors, and parameters, both calls return the same set, because annotations on those elements are never inherited regardless of the meta-annotation.

Putting It Together: A describe() Utility

The function below combines the pieces. It takes any class and prints a structured summary: class metadata, all declared fields with their modifiers and types, all declared methods with parameters and return type, and all declared constructors. It only reads; it does not invoke anything or change anything.

Every line came from a reflection call covered earlier: getModifiers, getSuperclass, getInterfaces, getDeclaredFields, getDeclaredConstructors, getDeclaredMethods, getType, getParameterTypes, getReturnType, and getDeclaredAnnotations. The describer is a small program built from the eight or nine accessors that make up the read-only reflection surface.

Two practical extensions: walk the superclass chain so that inherited fields and methods also show up, and filter by visibility to print "the public surface only." The inheritance walk:

The loop stops because Object.getSuperclass() returns null. The Object class has zero declared fields, so the section under it is empty.

Every reflection lookup costs more than a direct call. Class.getDeclaredMethod(name, types) does a linear scan of the methods array, allocates a defensive copy, and may trigger access checks. In hot code, cache the Method once at startup and reuse it; do not look it up on every invocation.

The final piece is filtering. To inspect only the public surface of a class, ignoring private helpers, Modifier.isPublic makes this a one-liner.

The same predicate pattern applies to constructors and fields. Predicates can also be combined: Modifier.isStatic and Modifier.isFinal together identify constants; Modifier.isAbstract on a class indicates that the class cannot be instantiated; Modifier.isInterface checks the interface bit on a class's modifiers.

Quiz

Inspecting Classes Quiz

10 quizzes