SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Applying Object-Oriented Principles in Java Programs

Java Objects, Nested Classes and Garbage Collection (1Z0-831)

14 min read1Z0-831 · Applying Object-Oriented Principles in Java ProgramsUpdated

An object's life cycle in Java runs from creation with new, through use while some live code can reach it, to eligibility for garbage collection once it is unreachable — and Oracle 1Z0-831 objective 3.1 asks you to trace that cycle in code and to create objects of every kind of nested class. This lesson separates references from objects, shows how aliasing and reassignment differ (including inside method calls), covers static nested, inner, local and anonymous classes with their exact instantiation syntax, and teaches how to decide which objects are eligible for collection at a given line.

What you’ll learn
  • Distinguish declaring a reference from creating an object, and count the objects a snippet creates
  • Trace aliasing, reference reassignment and pass-by-value of references through method calls
  • Instantiate static nested, inner, local and anonymous class objects with the correct syntax
  • Resolve shadowed names with this and Outer.this, and explain what this means in anonymous classes and lambdas
  • Apply the effectively final rule to variables captured by local and anonymous classes
  • Determine which objects are eligible for garbage collection at a given line, including cycles, arrays and inner-class links
  • Explain what System.gc() does and does not guarantee

Objects versus references: what new actually creates

An object exists only when an expression such as new Account() runs; declaring a variable creates a reference, never an object. A reference is a value that either points to one object or is null. new allocates the object, sets every field to its default (0, false, '\u0000' or null), runs the constructor chain and returns a reference to the result.

class Account {
    int balance;
    String owner;
}

public class Main {
    public static void main(String[] args) {
        Account a;                 // a reference, no object
        Account b = new Account(); // one object
        a = b;                     // still one object
        System.out.println(b.balance + " " + b.owner);
        System.out.println(a == b);
    }
}

This prints 0 null and then true. Only one Account object exists: a and b are two references to it, and == on references compares identity (the same object), not contents.

  • Count objects by counting new expressions that actually run (plus array creations and array initializers), never by counting variables. Lamp l1, l2 = new Lamp(); declares two references and creates one object.
  • A reference field that nothing assigns stays null. Calling a method through it (order.customer.name() when customer was never set) compiles, then throws NullPointerException at runtime.
  • A local variable gets no default: reading it before it is definitely assigned is a compile error, not a null.

Aliasing: changing an object versus reassigning a reference

When two variables hold the same reference they are aliases (say Tank t2 = t1;): a change made through either one changes the single shared object, while assigning a new reference to one variable changes only that variable. Keep the two operations apart when you trace code:

StatementWhat changesSeen through the alias?
t2.level = 40;A field of the object t2 points toYes — it is the same object
t1 = new Tank();Only the variable t1; it now points to a new objectNo — t2 still points to the old one
t1 = null;Only the variable t1No — the object survives while t2 refers to it

A reliable way to trace is to draw each object as a box and each variable as an arrow. Field assignments write inside a box; variable assignments move an arrow. Anything you compute afterwards reads the box the arrow currently points to.

Passing references to methods: pass-by-value of the reference

Java passes every argument by value, and for an object type the value copied is the reference. The method's parameter therefore starts as another alias of the caller's object: it can mutate that object, but reassigning the parameter only reseats the method's own copy and never reaches the caller's variable.

class Wallet {
    int coins;
}

public class Main {
    static void spend(Wallet w) {
        w.coins -= 3;      // reaches the caller's object
        w = new Wallet();  // reseats only the parameter
        w.coins = 50;      // lands on the new object
    }

    public static void main(String[] args) {
        Wallet mine = new Wallet();
        mine.coins = 20;
        Wallet shared = mine;  // alias: same object
        spend(shared);
        System.out.println(mine.coins + " " + shared.coins);
    }
}

The program prints 17 17. Inside spend, w.coins -= 3 lands on the one object that mine and shared both point to; after w = new Wallet() the parameter points to a brand-new object, so w.coins = 50 changes something the caller never sees. Two general rules follow:

  • Assignments to parameters never travel back. However a method reassigns or exchanges its parameters, the caller's variables still refer to exactly the objects they referred to before the call.
  • A field write goes to whatever object the parameter refers to at that moment. Trace each parameter's arrow line by line: if it currently points at one of the caller's objects, the write is visible to the caller; if it points at an object the method created, it is not.

The four kinds of nested class

A nested class is any class declared inside another class or block, and Java has four kinds that differ in whether they need an enclosing instance and where they can be created. A static nested class is declared with static at member level; every other nested class is an inner class — a non-static member class, a local class or an anonymous class.

KindDeclaredEnclosing instance?Created with
Static nested classMember level, with staticNonenew Outer.Nested() (or new Nested() inside Outer)
Inner (member) classMember level, no staticExactly one, fixed for lifeouterRef.new Inner(); just new Inner() inside an instance method of Outer
Local classInside a method, constructor or blockYes when declared in an instance contextnew Local(), only within its block, after the declaration
Anonymous classIn a new expression with a bodyYes when created in an instance contextnew Type(args) { … } — declared and instantiated at once

Member records, enums and interfaces are implicitly static. A record declared inside a class is a static nested type even without the keyword, so its methods cannot read the outer class's instance fields (non-static variable … cannot be referenced from a static context). Local records, enums and interfaces are implicitly static too, so they cannot capture the enclosing method's local variables.

Instantiating inner and static nested classes

An inner class object can only be created with an enclosing object to bind to, while a static nested class object needs none. Outside the outer class, or from a static method, you supply the enclosing instance with the qualified form outerRef.new Inner(); the type is written Outer.Inner.

class Garage{
    private int opens;
    private static int visits = 10;

    class Door {           // needs a Garage instance
        void open() { opens++; }
    }

    static class Inspector {  // no Garage instance
        String report(Garage g) {
            return g.opens + "/" + visits;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Garage g = new Garage();
        Garage.Door d = g.new Door();
        d.open();
        d.open();
        Garage.Inspector i = new Garage.Inspector();
        System.out.println(i.report(g));
    }
}

This prints 2/10: two calls through the Door bound to g raised g's private opens field, and the static nested Inspector read it through the explicit reference it was given. The legal and illegal forms are near-twins, so learn them precisely:

  • new Outer().new Inner() and ref.new Inner() compile — any expression of type Outer can be the enclosing instance.
  • new Outer.Inner() only names the type more fully; it does not supply an enclosing instance. In a static method it fails exactly like new Inner(): non-static variable this cannot be referenced from a static context.
  • new Inner() or this.new Inner() inside a static method fails: non-static variable this cannot be referenced from a static context. Inside an instance method, plain new Inner() works because it means this.new Inner().
  • new ref.Inner() is not the syntax — the compiler reads ref as a package or type name.
  • new Outer().new Nested() for a static nested class fails (qualified new of static class); use new Outer.Nested().

The binding is permanent: every inner object created from one outer object reads and writes that outer object's fields, so two inner objects made from the same outer object share its state, while one made from another outer object does not. A static nested class reaches the outer class's static members directly (even private ones) but its instance members only through an explicit reference. Since Java 16 an inner class may also declare static members; a static field in an inner class is one variable shared across every enclosing instance.

Shadowing and Outer.this

When an inner class declares a field with the same name as one in its enclosing class, the nearest declaration wins and Outer.this.name reaches the shadowed one. Inside an inner class, a simple name resolves to the innermost declaration: a parameter or local first, then the inner class's own field, then each enclosing class outwards. this always means the current (innermost) object; ClassName.this means the enclosing instance of that class.

class Shop {
    String label = "shop";

    class Shelf {
        String label = "shelf";

        class Slot {
            String label = "slot";

            String where(String label) {
                return Shop.this.label + "/" + Shelf.this.label
                    + "/" + this.label + "/" + label;
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Shop shop = new Shop();
        Shop.Shelf.Slot slot = shop.new Shelf().new Slot();
        System.out.println(slot.where("tag"));
    }
}

This prints shop/shelf/slot/tag. Each level of nesting has its own enclosing object, so with two levels Shop.this and Shelf.this are different objects, and the bare name label is the parameter because it is the innermost declaration. ClassName.this is a reference to the real enclosing object, not a copy taken when the inner object was created: whatever value that object's field holds at the moment of the read is what you get.

Local and anonymous classes: capture and the meaning of this

A local or anonymous class may read a local variable or parameter of its enclosing method only if that variable is final or effectively final — assigned once and never reassigned anywhere, before or after the class. Reassigning a captured variable is a compile error: local variables referenced from an inner class must be final or effectively final. Lambdas follow the same rule.

  • Loop variables. A classic for index is reassigned by its update expression, so it is not effectively final and cannot be captured; declaring it final just moves the error to the update. A variable declared inside the loop body and assigned once is a new, effectively final variable on each iteration, and so is the variable of an enhanced for — both can be captured.
  • An anonymous class can extend an abstract class: new Base(args) { … } passes the arguments to the superclass constructor and must implement its abstract methods. It cannot declare a constructor of its own; an anonymous class implementing an interface takes no arguments.
import java.util.function.Supplier;

class Radio {
    String tag = "radio";

    void tune() {
        String suffix = "!";   // effectively final
        Supplier<String> anon = new Supplier<>() {
            String tag = "dial";

            public String get() {
                return this.tag + "," + Radio.this.tag + suffix;
            }
        };
        Supplier<String> lambda = () -> this.tag + suffix;
        System.out.println(anon.get());
        System.out.println(lambda.get());
    }
}

public class Main {
    public static void main(String[] args) {
        new Radio().tune();
    }
}

This prints dial,radio! and then radio!. Inside an anonymous class body, this is the anonymous object itself (so this.tag is its own field, and Radio.this.tag reaches the outer one); inside a lambda, this is the enclosing Radio instance, because a lambda does not introduce a new object scope. The same split applies to an overridden toString() printed through this.

The object life cycle and garbage-collection eligibility

An object is eligible for garbage collection as soon as no live thread can reach it through any chain of references from a root. Roots are the local variables and parameters of methods that are still running, static fields of loaded classes and live threads. An object's life cycle is therefore: created by new, used while reachable, eligible once unreachable, and reclaimed at some later time the JVM chooses.

class Wagon {
    Wagon coupled;
}

public class Main {
    public static void main(String[] args) {
        Wagon front = new Wagon();   // wagon 1
        Wagon rear = new Wagon();    // wagon 2
        front.coupled = rear;
        rear.coupled = front;        // a cycle
        Wagon spare = new Wagon();   // wagon 3
        spare.coupled = new Wagon(); // wagon 4
        rear = null;
        front = null;                // 1 and 2: unreachable island
        spare = spare.coupled;       // 3: unreachable; 4: held
        System.out.println("parked");
    }
}

When parked is printed, wagons 1, 2 and 3 are eligible and wagon 4 is not (variable spare now holds it). Rules for tracing:

  • Only references from reachable things count. Wagons 1 and 2 point at each other, but nothing reachable points at either — an island of isolation, collectable however many references it holds internally. Add one incoming reference from a live variable or object and the whole island is reachable again.
  • Setting one reference to null makes an object eligible only if it was the last path to it.
  • Arrays are objects. Count the array itself, and an element is eligible once neither the array (if reachable) nor any variable refers to it.
  • Method exits end local references, but an object a method stored in a static field or returned to a caller who kept it stays reachable.

System.gc() and when memory is actually reclaimed

System.gc() (the same as Runtime.getRuntime().gc()) only suggests that the JVM expend effort recycling unused objects; it guarantees nothing. The Java SE API documentation says there is no guarantee it reclaims any particular objects or amount of space, or completes at any particular time — before the call returns or ever. Two consequences follow:

  • Eligibility and reclamation are different events. Your code decides eligibility by dropping references; the JVM decides if and when to reclaim. A reference cycle that is unreachable is eligible whether or not any collection ever runs.
  • Never rely on collection for program logic. Object.finalize() has been deprecated for removal since Java 18; release resources explicitly, for example with try-with-resources.

Inner objects keep their outer object alive

An inner class object is tied to its enclosing instance, so as long as the inner object is reachable, the outer object it uses is reachable too. Storing an inner object in a long-lived place — a static field, a listener list, a cache — therefore keeps the whole outer object (and everything it references, such as a large buffer) out of reach of the collector, even after every direct reference to the outer object is gone.

Under the hood the compiler stores the enclosing instance in a hidden final field (shown by javap -p as this$0). Since JDK 18, javac omits that field when the inner class never uses its enclosing instance (serializable inner classes excepted), so an inner class that touches no outer member no longer pins its outer object. Reason about code that does use outer members (as exam snippets do) as holding a reference. When an inner class does not need the outer object at all, declare it static: a static nested class has no enclosing instance and cannot cause this kind of leak.

Tip. Objective 3.1 is examined almost entirely through short, complete programs. Expect to read code and state what it prints, whether it compiles, or whether it throws — for example a call through a reference field that was never assigned, or an inner class created from a static context. Instantiation items list near-identical expressions (outerRef.new Inner(), new Outer.Inner(), new Outer.Nested(), this.new Inner()) and ask which compile. Shadowing items mix parameters, this.x and Outer.this.x across one or two levels of nesting. Garbage-collection items give a sequence of assignments and ask which objects, or how many, are eligible at a marked line — with cycles, arrays, static fields, returned objects and inner-to-outer links as the twists — or ask what System.gc() guarantees. Capture items reassign a local after an anonymous or local class uses it, or capture a loop index.

Key takeaways
  • A variable holds a reference, not an object; only new (or an array creation) creates objects, and unassigned reference fields are null.
  • Mutating through an alias changes the shared object; reassigning a variable changes only that variable.
  • Java passes references by value: a method can mutate the caller's object but can never reseat the caller's variable.
  • Inner class objects need an enclosing instance: outerRef.new Inner(); static nested classes use new Outer.Nested().
  • Member and local records, enums and interfaces are implicitly static and cannot use outer instance fields or captured locals.
  • Outer.this.name reaches a shadowed field of an enclosing instance; this in an anonymous class is the anonymous object, in a lambda the enclosing one.
  • Local and anonymous classes (and lambdas) may only capture final or effectively final locals.
  • An object is eligible once no live thread can reach it from a root; unreachable cycles are eligible too.
  • System.gc() is only a request — eligibility is guaranteed by your references, reclamation is up to the JVM.
  • A reachable inner object that uses its outer object keeps that outer object alive; prefer static nested classes when the link is not needed.

Frequently asked questions

Is Java pass-by-value or pass-by-reference?

Java is always pass-by-value. For object types the value copied into the parameter is the reference, so a method can change the object both variables point to, but assigning a new object to the parameter changes only the method's copy and the caller's variable keeps pointing to the original object.

How do you create an instance of an inner class from a static method?

You must supply an enclosing instance with the qualified form outerRef.new Inner(), for example new Outer().new Inner(), and the variable type is Outer.Inner. Writing new Inner() or this.new Inner() in a static method fails to compile because there is no this. A static nested class needs no instance: new Outer.Nested().

What is the difference between a static nested class and an inner class in Java?

An inner class (non-static member, local or anonymous class) is bound to an instance of its enclosing class and can use that instance's fields directly. A static nested class has no enclosing instance: it is created with new Outer.Nested(), sees the outer class's static members directly and reaches instance members only through an explicit reference. Member records, enums and interfaces are always static.

When is an object eligible for garbage collection in Java?

An object is eligible when no live thread can reach it through any chain of references starting from a root: a local variable or parameter of a running method, a static field or a live thread. Objects that only reference each other in a cycle are eligible too, because references from unreachable objects do not count.

Does System.gc() force garbage collection?

No. System.gc() suggests that the JVM spend effort reclaiming unused objects, but the Java SE API documentation gives no guarantee about which objects are reclaimed, how much memory is freed or when the work completes. Making an object unreachable is what makes it eligible; the JVM alone decides when it is reclaimed.

Why must variables used in an anonymous class be effectively final?

A local or anonymous class captures a copy of each local variable it reads, so Java requires that the variable is final or effectively final — assigned once and never changed — to keep the copy and the original identical. Reassigning a captured variable anywhere in the method is a compile error; the same rule applies to lambdas.

Can an inner class object cause a memory leak?

Yes. An inner class object that uses its enclosing instance holds a hidden reference to it, so storing the inner object somewhere long-lived, such as a static field or listener list, keeps the outer object and everything it references reachable. Declaring the nested class static removes that link.

Source

This lesson covers the "Applying Object-Oriented Principles in Java Programs" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?