Back to Blog
Java

Java Fully Qualified Class Name: Runtime Lookup

java fully qualified class name: Understand Java fully qualified class names: getName() vs getCanonicalName(), Class.forName() usage, and edge cases with inner classes...

JavaReflectionClassLoaderType SystemClass Name
Editorial diagram showing package segments joined by dots to form a Java fully qualified class name

java fully qualified class name requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

What a Fully Qualified Class Name Is

In Java, a fully qualified class name is the package name followed by the simple class name, joined by dots. java.util.ArrayList is the fully qualified name of the ArrayList class, and com.example.OrderService is the fully qualified name of a class declared in the com.example package.

The fully qualified name is the only name that uniquely identifies a class within a class loader. Two classes may share a simple name, but they cannot share a fully qualified name in the same class loader. That uniqueness makes the name the standard key for reflection, configuration files, and class loading operations.

Getting the Name at Runtime with Class.getName()

Every Class object exposes its binary name through getName(). The binary name is the form the JVM uses internally to locate and load the class.

package com.example; public class OrderService { public static void main(String[] args) { Class<?> clazz = OrderService.class; System.out.println(clazz.getName()); } }

This prints com.example.OrderService. The value is identical whether the Class object was obtained through the .class literal, instance.getClass(), or Class.forName("com.example.OrderService").

getName() vs getCanonicalName() vs getTypeName()

The Class API offers three methods that look interchangeable but diverge for nested classes, anonymous classes, and arrays.

MethodReturnsNested class separatorAnonymous classArray example
getName()Binary name$$1[Ljava.lang.String;
getCanonicalName()Canonical name.nulljava.lang.String[]
getTypeName()Delegates to getName()$$1java.lang.String[]

For a top-level class such as com.example.OrderService, all three return the same value. The differences surface only with nested classes, anonymous classes, and arrays.

public class Outer { public static class Inner { } } System.out.println(Outer.Inner.class.getName()); // com.example.Outer$Inner System.out.println(Outer.Inner.class.getCanonicalName()); // com.example.Outer.Inner System.out.println(Outer.Inner.class.getTypeName()); // com.example.Outer$Inner

The canonical name uses dots between nested classes, which reads naturally in logs and diagnostics. The binary name uses $, and it is the form Class.forName() requires.

Using the Name with Class.forName() and Reflection

Class.forName() takes a string and returns the matching Class object. The string must be the binary name, not the canonical name.

Class<?> clazz = Class.forName("com.example.Outer$Inner");

Passing com.example.Outer.Inner throws ClassNotFoundException because the class loader resolves classes by their binary name. This is a frequent source of bugs when code builds the name with getCanonicalName() and then feeds it to forName().

Edge Cases: Anonymous Classes, Arrays, and Primitives

Anonymous classes have no canonical name. getCanonicalName() returns null for them, while getName() returns a synthetic binary name such as com.example.Main$1.

Runnable task = new Runnable() { @Override public void run() { } }; System.out.println(task.getClass().getName()); // com.example.Main$1 System.out.println(task.getClass().getCanonicalName()); // null

Arrays follow different rules again. The binary name of String[] is [Ljava.lang.String;, which is not useful in human-readable output. The canonical name java.lang.String[] reads better but is not a valid argument for Class.forName().

Primitives behave consistently: int.class.getName() and int.class.getCanonicalName() both return "int".

Practical Considerations for Class Loading and Maintainability

Using fully qualified names in configuration files and reflection introduces a maintenance coupling: renaming a package or class breaks the string reference without any compile-time error. The compiler cannot validate the string passed to Class.forName(), so a typo in a configuration value surfaces only at runtime.

When the same class name is resolved repeatedly, cache the resulting Class instance instead of calling Class.forName() on every lookup. Each call performs class loading and verification work that is wasted when the same name is resolved again.

Choose the method by how the name will be used. Prefer getName() when the result will be passed back to Class.forName(). Prefer getCanonicalName() when the name is intended for logs, metrics, or other human-readable output. getTypeName() is convenient for formatting, but because it delegates to getName(), it does not produce canonical names for nested classes.

java fully qualified class name: Practical Usage and Code Ex | RYUSLOG DEV