What is a class? But first, what is an object?
In Java, values (whether literal or symbolic) may be either primitives or objects.1
A primitive value is simply that: a value. It has no intrinsic capability to do anything. For example, the int
value 212
(int
is a primitive type in Java, capable of holding integer values between -2,147,483,648
and 2,147,483,647
, inclusive) can’t add another int
to itself or subtract an int
from itself—let alone do any fancier tricks: multiplication, division, computing its own square root, and so on. To do any of that, we have to call on capabilities outside those primitive values.
A primitive value has no sense of self: If the int
value 212
is stored in memory somewhere, and another location in memory also holds the int
value 212
, there’s really no way to look at the 2 values and distinguish between them—not in Java, anyway. (This might not sound like a useful thing to be able to do, but it can be.)
In Java, the names of all of the primitive data types are spelled entirely in lowercase
—e.g., int
, double
, boolean
, char
. These types are defined by the Java language specification itself; we can’t modify these definitions or create any new primitive data types of our own. Apart from the boolean
type (which can only have one of the values true
or false
), all of the primitive types are numeric—even the char
type, which holds an integer value in the range 0
…65535
. For more information on the primitive types, see “Java Primitive Types”
An object, unlike a primitive, has skills. A String
object containing the character sequence " Hello, World! "
has the capability to construct a new String
object containing the same letters—but all uppercase, or all lowercase, or with all whitespace removed from the beginning and end. It can construct an array containing all of its individual characters. It can do a lot of other things, as well; we’re just getting started.
If we have 2 String
objects, we can distinguish between them, even if they have the same content—but either of them can still tell us whether its own content is equal to that of the other or not. Each can compare itself to the other, and tell us which should come first, if we need to sort strings in alphabetical order.
Generalizing from the examples above, we might say (correctly) that a primitive has state (its data content), while an object has state, behavior (what it can do), and identity (its sense of self, which doesn’t depend on its state).
How does an object learn its behavior, and set up the the elements of its state? If it were all pre-defined in the programming language, then we wouldn’t really be able to do much object-oriented programming (OOP): We couldn’t create any new types of objects, and we couldn’t modify the behaviors of the existing object types. (In fact, this is essentially the case with Java arrays.2)
Fortunately, in object-oriented languages (such as Java), we can create new types of objects. We can even build new types of objects based on those that already exist—taking the behavior that’s already defined, and adding to it or modifying it in other ways. In Java (and many other languages), we do this by writing classes.
Simply put, a class is an object type; the Java code we write for a class is the definition of that object type. Once we’ve written a class, we can create objects of that type—more formally, we create instances of the class.
(All of the classes in the Java standard library are named using UpperCamelCase
. This is also the convention followed by nearly all Java programmers when defining new classes; it’s a strict rule in Deep Dive Java training programs.)
When we define a type by writing the code of a class, we include elements in that definition for data that objects of that type will contain—that is, their state. If, for example, we want to have objects that represent 2-dimensional points on a plane, we would include fields (data elements) in the class to hold the X and Y coordinates of a point. Taken together, all the fields in a class define the potential state of any instance of that class.3
Every field in a class has its own type—which might be a primitive type, or an object type.
To define the behaviors of a class, we write methods. A method definition specifies the name of the behavior, the type(s) of data it needs as input, the type of data (if any) produced as output, and the instructions (code statements) that process the input in order to produce the output.
The instructions that we write—to be executed when the given behavior is requested (invoked is the term we’ll use most often)—make up the body of the method. The body of a method can be very simple; sometimes (especially if it’s early in the development of a class), we’ll even write methods that have no statements at all in the body. At the other extreme, the body of a method can include dozens—even hundreds (though that’s rarely a good idea)—of statements. These statements can perform basic arithmetic, logical, or string-oriented operations. They might perform some operations conditionally, and perform other operations repeatedly. They might invoke other methods to perform some part of the necessary task.
(By the way, when we take a task and break it down into sub-tasks, then implement separate methods for the sub-tasks, and finally implement a method that invokes the others in the proper sequence to perform the larger task, we’re decomposing that task. Decomposition is an important strategy in programming.)
When we define a class in Java, it always extends (is based on) an existing class; we call that existing class the superclass, and the new class a subclass of that superclass. If we don’t explicitly declare which class we’re extending, then the superclass is Object
, the most basic class in Java.
When we extend a class, we inherit the definition of its state and behaviors; they become part of the definition of the state and behaviors of the subclass.4 Any class has only one direct superclass (except for Object
, which has none), but we consider all of the classes in the inheritance chain of a given class—its direct superclass, the superclass of that direct superclass, and so on, all the way to Object
—to be the superclasses of that class. Thus, all Java classes form a single tree, with Object
at the root; this is the Java class hierarchy.
Once we’ve written the class that defines our new object type—assuming what we wrote is syntactically correct—we can write other code that instantiates (creates instances of) the class, and instructs those instances to perform the programmed behaviors (i.e., invokes the methods). In many cases, the code that consumes our class (creates instances of the class and invokes its methods) isn’t written by us at all: it might be written by other developers in our team, or in a client’s development team. We might not know anything about who will be using the class we’ve written; in that case, we’ll need to make sure that whoever does use the class knows what it’s capable of doing, and knows how to put those capabilities in action. The consumer might be code that’s already written, and we’ve written our class to satisfy an agreed-upon specification of how it should be instantiated and invoked—basically, we’re filling in a missing piece in a code puzzle in that case. If we write a class with a very specifically declared method, that method can be invoked by the Java application launcher itself, to start our code running as a Java application.
Obviously, there are many different ways a class can be written and used. Next, we’ll try to organize some of those uses into a few categories—and we’ll start to learn how the purpose of a class is reflected in how we declare its state and behaviors.
The only objects that can be expressed as literal values are strings—sequences of characters. ↩
In Java, every array is an object. However, much of the content in these pages isn’t applicable to arrays, since the underlying classes can’t be extended to define new types of arrays. For more information, see “Arrays in Java”. ↩
There can also be state at the level of the class itself, shared across all instances of the class; we’ll talk more about that later. ↩
Actually, there may be–and usually are—parts of the superclass’s definition that we can’t directly access in the code that makes up the definition of the subclass; for practical purposes, we usually don’t consider these to be inherited in the subclass. (This may not make much sense now, and there are other important details about inheritance that aren’t immediately obvious; but with practice, we’ll learn what we need to know.) ↩