Classes in Java: Definition Scope

Where can we define classes? Does that affect how we use them?

Overview

We can define classes at many different levels of our code. Where we choose to define a class (along with the modifiers we use in the declaration) affects how we can reference that class and its members in our code, and how the code of the class can interact with other classes.

Top-level classes

The first classes we encounter, in tutorials and basic exercises, are top-level classes. These are classes that are not nested within other classes or interfaces1.

As far as the Java compiler is concerned, there can be any number of top-level classes in a single source file; however, only one of these classes can be marked public. (see “Anatomy of a Class” for more information on access modifiers, and see this footnote in “Categorizing Uses” for one exception to this rule.) The name of this single public class must match the filename exactly, including letter casing. So, for example, a top-level public class named MyClass must be defined in a file named MyClass.java, and it must be the only public class in that file.

(In many organizational programming style guides—including the style guide we use in the DDC Java + Android Bootcamp, as well as the Google Java Style Guide, on which the DDC style guide is primarily based—only one top-level class is allowed per file.)

Nested classes

A class definition may be nested inside another class or interface—that is, the nested class is a member of the enclosing class. The same access-level modifiers we might use on a field, constructor, or method can also be used on a nested class. And just like fields and methods, the static modifier can be used with a nested class, to declare that instances of the nested class are associated with the enclosing class as a whole, rather than with instances of the enclosing class; without this modifier, every instance of the nested class is created within the context of some instance of the enclosing class, and is associated with that instance from that point on.

Static nested classes

A nested class declared with the static modifier is a static nested class. The code of the nested class has no access to non-static fields and methods of the enclosing class.

Static nested classes are essentially the same as top-level classes as far as the compiler is concerned; however, they can access private static members of the enclosing class, which other top-level classes cannot do. Similarly, if a static nested class is a private member of the enclosing class, then it cannot be seen (or instantiated) from outside that enclosing class.

We typically define classes of this kind when the enclosing class needs some kind of internal “helper” objects that are not relevant outside the class, or if it simply doesn’t make logical sense to treat the nested class as being wholly independent of the enclosing class.

Example

Here is a rather artificial example of a static nested class:

public class Outer {

  private static int x;
  
  private int y;
  
  public static class Nested {
  
    public void doSomething() {
      /*
       * Since Nested is a static member of Outer, its methods can
       * access other static members (such as x), but not non-static
       * members (e.g., y).
       */
    }
    
  }
  
}

Since the Nested class is a public static member of Outer, we can create an instance of Nested and invoke its doSomething method using code like this:

Outer.Nested n = new Outer.Nested();
n.doSomething();

Inner classes

A nested class that is not static is called an inner class, or sometimes a pure inner class. Instances of such a class cannot be created outside the context of an instance of the enclosing class; in many cases, we create instances of an inner class only in a constructor or non-static method of the enclosing class. Every instance of an inner class has an implicit reference to the associated context instance of the enclosing class, and can access static and non-static members of that class.

We typically use this kind of class when instances of the enclosing class make use of “helper” objects that require access to the instance state and behaviors of the enclosing class.

Example

public class Outer {

  private static int x;
  
  private int y;
  
  public Inner getInner() {
    return new Inner();
  }
  
  public class Inner {
  
    public void doSomething() {
      /*
       * Since Inner is a non-static member of Outer, its methods can
       * access static members (such as x) and non-static members (e.g., 
       * y) of Outer. Also, just as the current instance of Inner can
       * be referenced using "this", the current instance of Outer can 
       * be referenced using "Outer.this".
       */
    }
    
  }
  
  
}

In this example, an instance of Inner can only be created in the context of an instance of Outer. There are many ways we might do this; in this example, we can invoke the getInner method on an instance of Outer:

Outer out = new Outer();
Outer.Inner in = out.getInner();
in.doSomething();

Local classes

Like a local variable, a local class is one defined—and accessible—only inside the method, constructor, or initializer where it’s defined. If defined in a static method or static initializer, the local class is like a static nested class, in the sense that it can only access static members of the enclosing class. Otherwise, the local class is like an inner class, with access to the static and non-static members of the enclosing class. In both cases, the local class can access final and effectively final local variables of the enclosing method, constructor, or initializer.2

The following is an example of a local class in a method that sorts a String[] in ascending order by length, then alphabetically, without respect to case. The local class is acting as a comparator—that is, a type of object that knows how to compare pairs of objects (both of some other type), for the purposed of ordering or sorting.

void sortByLength(String[] words) {
  class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) {
      int result = Integer.compare(a.length(), b.length());
      if (result == 0) {
        result = a.compareToIgnoreCase(b);
      }
      return result;
    }
  }
  Arrays.sort(words, new LengthComparator());
}

Anonymous classes

An anonymous class is one that is defined and instantiated in a single statement. Like a local class, if an anonymous class is creaed inside a static method or static initializer, the anonymous class has no access to non-static members of the enclosing class; otherwise, it is like a local class in a non-static method, instance initializer, or constructor.

As the name indicates, an anonymous class has no name; it is defined in terms of its superclass (or an interface it implements), and is immediately instantiated—usually to be passed as an argument to a method, or to be assigned to a local variable or a field of the class.3

The local class example (above) can easily be rewritten to use an anonymous class:

void sortByLength(String[] words) {
  Arrays.sort(words, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
      int result = Integer.compare(a.length(), b.length());
      if (result == 0) {
        result = a.compareToIgnoreCase(b);
      }
      return result;
    }
  });
}
  1. An interface resembles an abstract class, but with some important differences—which we’ll examine in depth in the coming weeks. 

  2. An effectively final local variable is one which the compiler can conclusively determine will not be assigned more than one value in any given invocation of the method. Prior to Java 8, the compiler did not perform this analysis; thus, the only effectively final variables, pre-Java 8, were those explicitly declared as final

  3. In many cases, a lambda can be used as an an alternative to an anonymous class that implements a functional interface (such as Comparator). For example:

    void sortByLength(String[] words) {
      Arrays.sort(words, (a, b) -> {
        int result = Integer.compare(a.length(), b.length());
        if (result == 0) {
          result = a.compareToIgnoreCase(b);
        }
        return result;
      });
    }