Where can we define classes? Does that affect how we use them?
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.
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.)
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.
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.
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();
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.
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();
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());
}
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;
}
});
}
An interface resembles an abstract class, but with some important differences—which we’ll examine in depth in the coming weeks. ↩
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
. ↩
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;
});
}