Classes in Java: Categorizing Uses

What are classes used for? Does this affect how we declare a class and its members?

Overview

Typically, a discussion of classes in Java focuses heavily on object-oriented programming (OOP), and how the fundamental features of OOP are (or aren’t) implemented by the class mechanism of Java. However, this misses a basic point: Java classes aren’t just for OOP.

There are many different ways to categorize Java classes. Of course, the class hierarchy itself is an essential organizing tool. Another approach distinguishes classes by how an instance of a class is obtained by a consumer at runtime. Here, we’ll look at the intended purpose of a class, and how that purpose is reflected in the declaration of the class itself and in the declaration of its members.

Purposes

Let’s categorize classes into 4 general groups, corresponding to their purposes. These are not formal distinctions, but we find them useful nonetheless.

  1. A class may serve to define and include one or more entry points or (more generally) lifecycle methods for an executable unit of code. For example, execution of a Java application always starts with the loading of a startup class, and the invocation of the main method in that class.

    Entry points are usually very specific: the signature (name, parameter types, parameter number, parameter order), return type, and modifiers of the method must match those expected for the given type of entry point. So far, we’ve looked at Java applications, which have as their entry point a main method that is public and static, with a single parameter of type String[], and a void return type.1 There are other examples of lifecycle methods (e.g., the run method for a Java thread, the start and stop methods in a JavaFX application), as well; we’ll work with some of these later in the course.

  2. A class may act as a coherent collection of methods, related by the nature of the operations they perform and the types of data involved. We might think of this type of class as a drawer in a mechanic’s toolbox, filled with tools for performing a set of related tasks.

    There are many examples in the Java standard library of classes falling into this category. Three that we tend to use very frequently are these:

    • java.lang.Math includes static methods for performing and returning the results of mathematical computations on values of the primitive numeric types.

    • java.util.Arrays has static methods for performing a number of utility operations (sorting, copying, etc.) on arrays.

    • java.util.Collections is, in many ways, analogous to java.util.Arrays, providing methods for the creation and use of Collection objects—instances of implementations of one of the List, Set, and Map interfaces (all of which are subinterfaces of Collection).

  3. We can define a class for the purpose of creating a new type of object, with specialized state (fields or attributes) and behavior (methods). When we create objects based on such a class, the objects are referred to as instances of the class.

    In some programming languages, such a class can be an entirely new type—that is, its definition isn’t based on any other class. In Java, however, all classes are defined in a class hierarchy, with java.lang.Object at the top; if we don’t explicitly extend an existing class when we define a new one, the new class implicitly extends the Object class. This kind of class definition—extending a general class by defining a more specialized one—is what we’re usually talking about when we discuss classes in the context of object-oriented programming.

    Definition of a new type based on another, where the new type automatically has the behaviors and attributes of the previous, is called inheritance. Further, if type B is based on type A, and we can treat an object of type B as if it were an object of type A, then it’s also subtyping (aka inheritance of type), which is one kind of polymorphism. (In this context, type and class are synonymous.) In Java programming, inheritance, polymorphism, and subtyping are all related.

    We can see an illustration of analogous concepts in the taxonomic relationship between the domesticated dog and the gray wolf. Canis lupus familiaris (dog) is a subspecies of Canis lupus (gray wolf), and many of the attributes and behaviors of Canis lupus are shared by Canis lupus familiaris; this is inheritance, in OOP terms. In most contexts, we treat dogs as dogs. However, dogs are still members of the species Canis lupus, and in some contexts, we treat them as such, without distinguishing them from wolves; this is subtyping. Finally, there are some behaviors which, while shared (in general terms) by both dogs and wolves, are distinctively different between the two; this is a kind of polymorphism, where behaviors with the same name differ, depending on the type of object.

    You’ll create lots of classes of this kind in your work—in this course and beyond. Also, many of the already defined classes that you’ll use fit into this group. For example, one of the first classes that Java programmers are introduced to is java.lang.String. In Java, a string—even literal quoted text, like “Hello World!”—is actually an instance of the String class, with identity (two strings with identical text content are still distinguishable from each other), state (the text content), and behavior (methods that can be used to retrieve information about the state, or modify the state—in the case of String, an immutable class, methods that modify the state actually create and return a new String with that modified state).

  4. Finally, we have a category that has a lot of overlap with the previous category—but the distinctions are important enough that it can be useful to treat this as a category of its own: We can define a class not primarily for the purpose of being able to create and use instances of that class in our code, but in order for other, more specialized classes to extend it further. In some cases, we will even make it impossible to create instances of the more general class we’re defining.

    Taking another example from the animal kingdom, consider the genus Felis: there are many species in the genus (including Felis catus, the domestic cat, and several species of small wild cat), and while there are attributes and behaviors that are common to all members of the genus, there aren’t any actual cats that are just of that genus; instead, there are cats belonging to one of the species in the genus. Nonetheless, it makes sense to have the genus defined, and to identify the distinguishing characteristics (attributes and behaviors) of the genus, even though we know that any actual cats in the genus will be more specialized than that. When we define classes like this in Java, we call them abstract classes.

Many classes primarily in the 1st or 2nd category are not intended to be used to create objects, or to be further specialized; thus. the position of such a class in a hierarchy of types is secondary. For a rather trivial example, a typical HelloWorld class serves as an entry point to a very simple Java application, and is not used to create new instances of the HelloWorld class; thus, the fact that the HelloWorld class is a subclass of Object isn’t very important to us. Many Java application startup classes have this in common; in fact, a common practical complaint about Java is that running even a very simple application requires a class, even though its purpose may have little to do with object-oriented programming.

The java.lang.Math class is an example of the 2nd category of class: it can’t be used to create Math objects—instead, it operates on values of the intrinsic int, long, float, and double types—and can’t be specialized further by creating subclasses of Math. (Of course, that doesn’t mean we can’t write a different class to perform similar or improved mathematical computations, and use that class in place of Math.)

The above categories aren’t mutually exclusive. In practice, many of the classes you’ll create, and many of the existing classes you’ll use, fit into more than one of the categories. However, this categorization can be a useful way of looking at a class—in terms of its primary purpose, and how that purpose is reflected in the code.

Constructors

When the purpose of a class is to define an instantiable object type, we often include non-private (see below) constructors in the class definition.2 The constructor’s job is to initialize the state of an instance of the class. A constructor looks like a method, with a couple of important differences:

Modifiers

Access modifiers

Sometimes, the categorization of a class’s intended purpose is reflected in access modifiers used in the class definition, and in the definitions of the constructors and members (fields, methods, and nested classes) of the class.

public

A public class is one that is visible outside the package where it’s defined. Most classes intended for use as entry points are public. Similarly, if we write a class as a collection of related fields and methods, intending it for use as a toolbox for other developers, it would probably defeat that purpose if we don’t declare the class public.

On the other hand, a class whose purpose is to define a new object type may or may not be public, depending on our needs. In some cases, we might define a new object type that is only intended for use in our own code, and just in the package where the class is defined; in that case, we might not make the class public.

public can also be used with class members and constructors. This serves to make them visible outside the class—even outside the package where the class is defined.

(Note that Java allows no more than one public class to be defined in a .java source file.3 As a matter of practice, many organization’s style guides—including Deep Dive Coding’s style guide—take that one step further, and allow no more than one top-level class—public or not—in a source file.)

protected

The protected access modifier can be used on a member of a class (but not on a top-level class), to make that member accessible from other code in the same package, and from subclasses of the class. Thus, we tend to use protected only when we’re defining a class that falls at least partly in the 4th category—that is, a class that we anticipate will be further specialized by subclasses.

package-private (default)

When no access modifier is included in a class or member definition, the default package-private access rule is in force. With this access level, classes are visible and accessible to code in the same package; similarly, class members with package-private access are visible and accessible to code in the same class and in the package in which the class is defined.

private

We can use the private modifier on members of a class. By doing that, we make those class members visible only inside the class; code in other classes—even in the same package—can’t access those members. (In the case of a nested class, private members are also visible in the enclosing class; similarly, private members of an enclosing class may be visible in the nested class, depending on the static vs. non-static modifiers on the nested class and the other members of the enclosing class. See “Scope” for more details.)

When we’re defining a class that’s entirely in the 2nd (or sometimes the 1stcategory—that is, when we aren’t using it to define a new type of object—we often define a private constructor with no parameters. (Note that if we don’t define any constructors at all, the Java compiler defines—in the byte code—a public constructor with no parameters as a default constructor.)

Other modifiers

In addition to the access modifiers, there are several other modifiers that can be applied to classes and members—but not constructors. (Formally, constructors are not considered class members—primarily since they aren’t inherited.) Some of them reflect the purpose of a class, and its categorization in the above scheme; these are summarized here.

static

This modifier is used on class members, to associate them with the class as a whole, rather than with objects created from the class. We’ve already seen one very important use of this modifier, in Java applications: The Java application launcher expects to find a main method in the startup class—one that is not only public (otherwise, the launcher won’t be able to see it) but also static (so the launcher can invoke it immediately after loading the class itself into memory, without having to create an instance of the class).

Within a non-static method, the context instance can be referenced via the this keyword. (Note that when doing so does not result in ambiguity, the this keyword can be omitted from code in instance methods when referring to fields and other methods of the same instance; it’s generally considered good practice to avoid using this unless it’s needed to resolve ambiguity.) However, in a static method, there’s no instance to reference; thus, this can’t be used. On the other hand, in both static and non-static methods, the current class can be referenced by name—that is, since static members are accessible in the context of the class, they can be referenced via the class name. (Once again, if no ambiguity would result, the class name can be omitted from code in instance or static methods, when referring to static fields and methods of the same class.)

A class that’s primarily in the 2nd category, but not the 3rd or 4th, will often have only static fields and methods, since it might be neither necessary nor desirable to create instances of the class (and thus, non-static fields and methods will never be used anyway). Many other classes will have a combination of static and non-static members. For example, the java.awt.Color class has static fields (representing specific color constants), some static methods (used for various color lookups and conversions), and some instance methods (primarily for obtaining information about Color instances).

abstract

abstract can be applied to classes, and to methods (if used with a method, it must also be applied to the class as a whole). An abstract method is one which is is not yet implemented: all that appears is the declaration, with no method body. An abstract class is one which cannot be instantiated—that is, we can’t create object instances of that class. Instead, we must define a subclass of the abstract class, implementing any abstract methods in the process, and then create instances of the subclass. Thus, a class of this type falls primarily into the 4th category: its purpose is to establish a point for further specialization by one or more subclasses in the class hierarchy.

final

This modifier can be applied to classes and members. When applied to a class, it specifies that no further specialization is allowed—that is, subclasses of a final class can’t be defined. When applied to a field, it indicates that once a value is assigned to that field (either in the declaration, or in a constructor, or in an initializer—more about that last one later), it can’t be changed. When assigned to a method, it means that the method can’t be overridden or hidden in a subclass.

Applied to fields, final is often used in combination with static to define a constant. For example, in java.lang.Math, there is a declaration for public static final PI, with an assigned value of 3.141592653589793. If we attempt to write code that assigns a new value to Math.PI, compilation will fail, since the value of a final field cannot be modified after the initial assignment.

As noted above, when applied to methods and classes, final is used to limit (or eliminate entirely) the possibility of further specialization. At the class level, this would only be done for a class that is not in the 4th category—possibly because it was never intended to be extended, or because the the developer feels that it will be difficult to specialize it further without causing problems (e.g., violating the Liskov substitution principle4).

Summary

Most of the time we spend programming in Java is focused on creating classes of one kind or another. However, the clearer we are from the start about the intended purpose of a class, the better the chance that we can deliver a well-written class early in the development cycle, with minimal effort later spent on reworking the code in an effort to “get it right this time.”

  1. The main method is not just the entry point for a Java application; it is also the only standard lifecycle method common to all Java applications: The main thread of an application starts with the invocation of the main method, and it completes when main returns, or when it terminates through an exception. 

  2. Implementation of some creational design patterns enable instantiation by a client not through direct access to one or more constructors, but through the use of builders, factories, etc. In such implementations, it’s typical for constructors to be declared with private or package-private access. 

  3. There is an exception to this rule: Java 11 introduced the single-file source-code application into Java. This is a simple application or script, compiled and executed in a single step by the java application launcher. This type of Java source code file may contain multiple public classes. 

  4. The Liskov substitution principle states:

    Let $\phi (x)$ be a property provable about any object $x$ of type $T$. Then $\phi (y)$ should be true for any object $y$ of type $S$, where $S$ is a subtype of $T$.

    In other words, code that works with instances of T should be able to work just as well (without any modification) with instances of S, if S is a subtype (subclass, subinterface, or implementing class) of T.

    This is a very useful guideline to follow in OOP