Java Anonymous Classes

An anonymous class in Java is a class without a name that you declare and instantiate in a single expression. Instead of writing a separate .java file (or even a named inner class) to implement an interface or extend a class just once, you write the implementation right where you need it. Anonymous classes are heavily used in event handling, comparators, and callback-style APIs, and understanding them is essential for reading older Java code and for appreciating what lambdas (introduced in Java 8) simplify.

Overview / How it works

Normally, using an interface or an abstract class requires three steps: declare the interface, write a class that implements it, and instantiate that class. An anonymous class collapses the last two steps into one. You write new SomeType() { ... }, where SomeType is either an interface or a class (concrete or abstract), and the block { ... } is the body of a brand-new, unnamed class that implements or extends SomeType right there.

Under the hood, the compiler still generates a real .class file for every anonymous class — it just gives it a synthetic name like Main$1, Main$2, and so on (a number appended to the enclosing class name, in declaration order). At the bytecode level there is nothing special about an anonymous class: it is a normal class that happens to have no accessible name in source code. It can have fields, extra methods, and instance initializer blocks, but it cannot declare a constructor with parameters of its own (because there is no name to give the constructor), and it cannot implement more than one interface or extend a class while also implementing an interface.

Anonymous classes are a special case of inner classes. Like other non-static inner classes, a non-static anonymous class keeps an implicit reference to the enclosing instance (so it can access the outer object’s fields and methods), and it can read local variables from the enclosing method — but only if those variables are final or effectively final (never reassigned after initialization). This restriction exists because the local variable’s value is actually copied into the anonymous class instance at construction time; if the original variable could change afterward, the copy and the original would silently drift apart.

Syntax

InterfaceOrClass reference = new InterfaceOrClass(constructorArgs) {
    // field declarations
    // overridden / new method bodies
    // instance initializer blocks
};
Part Meaning
InterfaceOrClass The interface to implement, or the class (abstract or concrete) to extend. This becomes the compile-time type of reference.
(constructorArgs) Arguments passed to the superclass constructor. Empty () for an interface or a class with a no-arg constructor.
{ ... } The class body: must override every abstract method it is required to implement; may add extra members, but those extras are only reachable from inside the block, since the outside reference is typed as InterfaceOrClass.

Because the anonymous class has no name, you can never write a constructor for it explicitly, and you can never refer to its type again after the statement that creates it — the variable holding it is always typed as the interface or superclass.

Examples

Example 1: Implementing an interface anonymously

public class Main {
    interface Greeting {
        void greet(String name);
    }

    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            @Override
            public void greet(String name) {
                System.out.println("Hello, " + name + "!");
            }
        };
        greeting.greet("Alice");
    }
}

Output:

Hello, Alice!

Here Greeting is a functional interface with one abstract method. Instead of writing a named class like FriendlyGreeting implements Greeting, the implementation is supplied inline as an anonymous class, and greeting is immediately usable.

Example 2: Extending an abstract class anonymously

public class Main {
    abstract static class Animal {
        abstract String sound();

        void describe() {
            System.out.println("This animal says: " + sound());
        }
    }

    public static void main(String[] args) {
        Animal cat = new Animal() {
            @Override
            String sound() {
                return "Meow";
            }
        };
        cat.describe();
    }
}

Output:

This animal says: Meow

This time Animal is an abstract class, not an interface. The anonymous class only needs to fill in the missing abstract method (sound()); it automatically inherits the concrete method describe() from Animal.

Example 3: A realistic use — custom sorting with Comparator

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Charlie");
        names.add("alice");
        names.add("Bob");

        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.compareToIgnoreCase(b);
            }
        });

        System.out.println(names);
    }
}

Output:

[alice, Bob, Charlie]

Before Java 8 this was the standard way to pass custom sorting logic: Comparator has a single abstract method, compare, and an anonymous class implementing it can be dropped directly as an argument to Collections.sort. This exact pattern is still common in older codebases and in APIs that predate lambdas.

How it works step by step / Under the hood

  • The compiler encounters new Type() { ... } and generates a separate class file, e.g. Main$1.class, whose superclass or implemented interface is Type.
  • If the anonymous class is non-static (declared inside an instance context), the generated class gets a hidden field holding a reference to the enclosing instance (Main.this), so it can call outer methods and read outer fields.
  • Any effectively-final local variables the block uses are copied into hidden constructor parameters and stored as hidden final fields on the generated class — this is called capturing.
  • The compiler inserts a synthetic constructor that accepts the outer-instance reference (if needed) and the captured variables, then calls it at the new Type() { ... } site.
  • At runtime, the JVM loads the anonymous class like any other class and creates an instance of it; calling a method on the reference dispatches to the anonymous class’s override, exactly like normal polymorphism.

Common Mistakes

Mistake 1: Capturing a variable that isn’t effectively final

Local variables used inside an anonymous class must never be reassigned after their initial value is set, or the code will not compile.

public class Main {
    interface Counter {
        void increment();
    }

    public static void main(String[] args) {
        int count = 0;
        Counter counter = new Counter() {
            @Override
            public void increment() {
                count++; // Compile error: count is not effectively final
                System.out.println(count);
            }
        };
        counter.increment();
    }
}

The fix is to hold the mutable state in something the anonymous class can mutate through a reference, such as an AtomicInteger, a single-element array, or a field on an enclosing object:

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    interface Counter {
        void increment();
    }

    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        Counter counter = new Counter() {
            @Override
            public void increment() {
                System.out.println(count.incrementAndGet());
            }
        };
        counter.increment();
        counter.increment();
    }
}

Output:

1
2

count itself is now a final reference (it is never reassigned), even though the object it points to is mutable — that satisfies the effectively-final rule.

Mistake 2: Calling extra methods that aren’t part of the declared type

Any member you add inside the anonymous class body that is not part of the interface or superclass is invisible outside the block, because the variable’s compile-time type is the interface/superclass, not the anonymous class.

public class Main {
    interface Greeting {
        void greet();
    }

    public static void main(String[] args) {
        Greeting g = new Greeting() {
            @Override
            public void greet() {
                System.out.println("Hi");
            }

            public void extra() {
                System.out.println("Extra method");
            }
        };
        g.extra(); // Compile error: extra() is not visible on type Greeting
    }
}

Since the anonymous type has no name to declare a variable with, you cannot expose extra() to outside callers. The usual fix is to call any extra helper methods from inside the overridden method itself (or an instance initializer), not from outside the block:

public class Main {
    interface Greeting {
        void greet();
    }

    public static void main(String[] args) {
        Greeting g = new Greeting() {
            @Override
            public void greet() {
                extra();
            }

            private void extra() {
                System.out.println("Extra method");
            }
        };
        g.greet();
    }
}

Output:

Extra method

Best Practices

  • Prefer a lambda expression over an anonymous class whenever the target type is a functional interface (exactly one abstract method) — it is shorter and clearer.
  • Reach for an anonymous class when you need to implement an interface with more than one abstract method, extend an abstract class, add extra fields, or use an instance initializer — none of these are possible with a lambda.
  • Keep anonymous class bodies short. If the logic grows past a few lines, extract a proper named (possibly private static nested) class instead — it will be easier to test and reuse.
  • Remember that a non-static anonymous class holds a hidden reference to its enclosing instance; if you store such an anonymous instance somewhere long-lived (e.g., a static collection), it can keep the whole outer object alive and cause a memory leak.
  • Give overridden methods the @Override annotation so the compiler catches signature typos immediately.
  • Avoid capturing large or frequently-changing state; each captured variable is baked in as a snapshot at creation time, which can be a source of confusion if you expect live updates.

Practice Exercises

  • Exercise 1: Define an interface Shape with a method double area(). Using an anonymous class, create a Shape representing a circle of radius 5.0, and print its area.
  • Exercise 2: Define an abstract class Vehicle with an abstract method String describe() and a concrete method void printInfo() that prints the result of describe(). Create an anonymous subclass representing a “Bicycle” and call printInfo().
  • Exercise 3: Given a List<Integer> of unsorted numbers, use Collections.sort with an anonymous Comparator<Integer> to sort the list in descending order, then print the result.

Summary

  • An anonymous class lets you implement an interface or extend a class in a single expression, without declaring a separate named class.
  • The compiler generates a real class file for it (e.g., Main$1) — there is no runtime magic, just no accessible source-level name.
  • Non-static anonymous classes capture a reference to the enclosing instance and copies of any local variables they use; those local variables must be final or effectively final.
  • You cannot write an explicit constructor for an anonymous class, and members it adds beyond the declared type are not accessible from outside the block.
  • Since Java 8, lambdas replace many anonymous-class use cases for functional interfaces, but anonymous classes remain necessary for multi-method interfaces, abstract classes, and cases needing extra state or initializers.