mattone
dopo mattoneTHE BOOK SERIES
IT/EN
← Java guide

Java 25 · 10/39

10. Object, wrapper classes and enumerations

Java 25 · Complete guide · Draft under review

This guide retains the book’s draft status. Editorial review and comprehension checks with independent readers remain to be completed.

Search the whole guide →

A common ancestor, three different questions

In chapter 7 we saw that two variables can refer to the same object. In chapter 9 we introduced a class hierarchy. At the root of Java classes is Object: even when we do not write extends Object, a class without another superclass implicitly extends it. This gives every instance methods such as equals, hashCode and toString. It does not mean, however, that these methods always answer the question our program cares about.

== between references asks whether they identify the same object. equals asks whether two objects are equal according to their class's criterion. Behavior inherited directly from Object considers only references to the same object equal; a class can override it. toString produces a textual representation useful for reading an object. Its base implementation is not a description of the object's contents: if we want to show it with a clear meaning, we override it.

In the complete program we model a small Label whose value comes from the text it contains. We construct two with the same text, but in two separate new expressions. The identity comparison is false; the value comparison is true. Figure 10.1 connects the three questions to the three expressions without suggesting that a hash is an identity.

Identity, equality and hash
Figure 10.1 – Two different labels can have the same value. An equal hash code is required for equal objects; alone it does not prove the objects are equal.

Giving equals and hashCode a meaning

The example uses an immutable class: after construction, its text field does not change. This makes the equality contract easier to respect. equals first checks identity, then verifies that the other object is a Label; finally it compares the texts with String.equals.

@Override
public boolean equals(Object other) {
    if (this == other) return true;
    if (!(other instanceof Label label)) return false;
    return text.equals(label.text);
}

@Override
public int hashCode() {
    return text.hashCode();
}

The signature receives Object because that is the method we override. The instanceof pattern, encountered in chapter 9, avoids a separate cast. Two Label objects with the same text obtain the same hash because we calculate it from the same data used by equals. If we overrode only equals, a hash-based collection could search for equal objects in different positions and behave unexpectedly.

Key concept – The equality contract. equals must be reflexive, symmetric, transitive and consistent as long as the compared data do not change. If a.equals(b) is true, a.hashCode() and b.hashCode() must be equal. The reverse is not required: two different objects can produce the same hash number. The Object specification presents the complete contract.

Two points with the same coordinates

Construct new Point(10, 10) twice. Before overriding equals, the two points are distinct instances and the method inherited from Object returns false. After an override comparing x and y, the result becomes true. At this point we must consider hashCode: if the two points are equal according to equals, they must produce the same hash code. Our Label follows exactly this progression, using a single component to make the contract visible.

A hash code is not a unique identifying number for the object. It returns an int, so many possible real-world values must share some result, and two different objects can have a collision. A hash-based structure uses the number to narrow the search and must then check equality. Even the default toString representation, showing the class name followed by @ and a hexadecimal form of the hash code, does not demonstrate that two objects are identical or different. The sequence must not be interpreted as the instance's memory address.

Mutability makes this promise harder. If we insert an object into a hash-based collection and then change a field used by both equals and hashCode, a search may no longer find it where it was placed. This is why Label keeps immutable text. It is not the only possible solution, but reduces a concrete risk that too small an example might hide.

For classes with several fields, the standard library's Objects.equals and Objects.hash can simplify code. We do not automatically add every field: first choose what makes two instances equivalent in the domain. If equality must be based on all components of a simple immutable value, a record like the one in chapter 7 already provides equals, hashCode and toString consistent with the components. If a component is an array, the record-generated comparison still uses that component's equality: designing the value remains our responsibility.

In the example toString returns Label[Java], so a print or diagnostic message is readable. It should not reveal confidential data: text printed by an object can end up in logs and error reports.

getClass() returns a Class object describing the instance's actual class at runtime. It can serve diagnostics or APIs examining types, but normal program logic should prefer contracts, methods and interfaces to constantly querying the concrete class. clone() exists in Object, but its combination with Cloneable and with objects containing other objects needs care: it is not a universal command for obtaining an independent copy. For a simple value, constructing a new object or using a copy method with an explicit contract is often clearer.

On the Class object obtained from a Point we can call getSimpleName(), getName() and getPackageName(). The distinction appears with a class in the examples.geometry package: the simple name may be Point, while the full name includes the package. This information can help a system recording events or loading components by name, but does not say which values a particular Point instance contains. Reflection allows examining members and, within permitted limits, invoking operations; it does not arbitrarily rewrite the source code or public interface of an already-loaded class, as an overly generic description might suggest. Its power must be accompanied by access rules, modules and error checking.

Object also declares methods associated with coordination between threads, such as wait and notify. We will not use them to learn equality: they belong to concurrency and require understanding monitors, lock ownership and waiting conditions. A method's being inherited by every object does not mean it is appropriate to call it in any chapter.

Three methods, three different tests

Consider two points with coordinates (10, 10). Without an override, first.equals(second) inherits Object behavior and returns false if we used two new statements, even though coordinates match. After an override considering only x and y, the result becomes true. At that point we must also override hashCode using the same data. We do not need a “beautiful” hash number or a different one for every point: we need to guarantee that two points considered equal produce the same number. If this rule is missing, the defect may remain invisible until we insert the points into a hash-based collection.

The third test concerns toString. Printing System.out.println(first) invokes a textual representation of the object. We can decide to return Point[x=10, y=10], which helps diagnosis; this choice changes neither identity nor equality. Two points can have the same toString and remain distinct, or be equal according to the contract and have a representation intended to change for editorial reasons. We do not write a program comparing objects through their printed string: the three questions must remain separate.

getClass() answers a fourth question: what is the actual class of the instance we are observing? If a variable has type Object but indicates a Point, getClass() describes Point. The result is a Class object, not a string to compare by hand. getSimpleName() can produce only the simple name and getName() a fuller name useful in diagnostic messages; neither tells us whether two points have equal coordinates. In the inheritance chapter we saw that working through a general type is normal: resorting to getClass() for every domain decision would often mean losing the benefit of common methods and overriding.

A final distinction concerns the hash code. hashCode() does not promise to return the object's address or a different number for every instance. A HashSet can place two different values in the same area of its structure and then distinguish them with equals. The contract requires only equal objects to have equal hashes during the period when relevant data do not change. This is why our example avoids printing specific hash numbers as expected output: the reader must verify the relationship between values, not memorize a number produced by one particular execution.

Further reading – Objects.equals and the null case. If two fields can be null, Objects.equals(a, b) returns true when both are null, false when only one is, and otherwise calls a.equals(b). It reduces repetitive checking, but does not decide which fields belong to the value's meaning. Objects.hash(...) builds a hash from supplied values; here too value selection must remain consistent with equals. Similar naming does not authorize using Objects.equals in equals and a different field in hashCode.

When a primitive is needed as an object

Primitive types, for example int, are not Object instances. Wrapper classes (Integer, Double, Boolean and the others) represent those values as objects. In the example, Integer.valueOf(42) produces an Integer; assignment to int value extracts the primitive through unboxing. The reverse operation, from primitive to wrapper, is called boxing. The compiler can insert them automatically when context requires.

Integer number = Integer.valueOf(42);
int value = number;
System.out.println(value + 1);

The line prints 43. If number were null, unboxing would have no value to extract and would produce NullPointerException. Furthermore, == applied to two wrapper references compares identity and can appear to give different results for different values because of caching rules. To compare value, use equals or, when values are valid and conversion is appropriate, compare primitives. The java.lang documentation lists wrappers; the collections we will see later are a common use case.

Important note – Mixed comparisons. In number == 42, one operand is primitive and Java can extract the wrapper value; in number == anotherInteger, if both are Integer references, the comparison is identity-based. Writing types and conversions explicitly while learning avoids attributing a single meaning to == in different contexts.

Why wrappers exist

A collection such as List<Integer> works with objects, while an int[] array directly stores primitive values. Wrappers allow numeric values to be used in contexts requiring references. The correspondence is byte/Byte, short/Short, int/Integer, long/Long, float/Float, double/Double, char/Character and boolean/Boolean. The names Integer and Character are not obtained merely by capitalizing the primitive's first letter: it is good to recognize them when they appear in compiler messages.

The value represented by a wrapper does not change after creation. If we write Integer number = 10; number = number + 1;, the second statement does not internally modify the object that represented 10: it calculates a new value and assigns the variable an appropriate reference for 11. The compiler can hide boxing and unboxing in brief syntax, but the conversions exist and can have costs or fail if a wrapper to unbox is null. For explicit text-to-number conversion we will use methods such as Integer.parseInt("42"), distinguishing a nonnumeric-text error from the mere presence of a wrapper object.

The wrapper table makes the correspondence evident without suggesting that Boolean and Character belong to the numeric hierarchy.

Primitive value Wrapper object One possible use
byte, short, int, long Byte, Short, Integer, Long integer value in an API requiring objects
float, double Float, Double floating-point value in a collection
char Character UTF-16 unit treated as an object
boolean Boolean logical result in a context permitting null

The table groups pairs by function, but each primitive has its own wrapper. Integer does not contain an int to change through a setter. To obtain an int from text we use Integer.parseInt, which returns a primitive; Integer.valueOf instead returns an Integer object. Both can reject nonnumeric text with NumberFormatException. If we need to represent absence of a value, Integer can contain null as a reference, but this requires an explicit choice before unboxing. A missing value should not automatically be confused with zero.

Figure 10.2 shows the wrapper hierarchy: numeric wrappers fall under Number, while Boolean and Character are separate. The drawing does not say all numeric values are interchangeable without conversion; it shows only the relationship between wrapper classes.

Wrapper hierarchy
Figure 10.2 – Boolean and Character are not subclasses of Number. Every primitive has a wrapper, but the class hierarchy does not coincide with a ranking of numeric conversions.

To understand – An error hidden by syntax. Consider Integer grade = null; int next = grade + 1;. The second line looks like ordinary addition, but first requires extracting an int from grade. Since there is no object from which to extract the value, NullPointerException results. The message does not say the + operator is defective: it invites us to trace the null reference's origin and decide what meaning it has in the contract.

Following boxing and unboxing without magic

Imagine adding a grade to a List<Integer>. The collection stores objects, while the value you calculated may be an int. In a call such as grades.add(27), the compiler inserts boxing to obtain an Integer. When you read int first = grades.get(0);, it inserts unboxing. The code is short, but careful reading remembers that get(0) returns a reference and that this reference could be null if the collection permits and contains null. A compiler conversion does not change the contract of the data you chose to store.

Comparison is another point where short syntax misleads. Integer a = 100; Integer b = 100; can make a == b appear true, because some instances are shared according to API rules. Changing the values can make the same comparison between variables containing equal numbers appear false. A book should not teach the cache boundary as a rule for comparing numbers: it should teach how to ask the right question. If you want to know whether two wrapper values represent the same number, use a.equals(b) after defining how to handle null. If you want to work only with present numbers and choose a primitive comparison, extract values deliberately. == between two references remains an identity comparison.

Double and Float also require different care from integers: floating point has special values and its own rounding. A wrapper does not magically make calculation exact. When we deal with monetary amounts, replacing double with Double will not suffice; we will need to choose a representation consistent with amount rules. Here it is enough to recognize that the wrapper solves the object type problem, not every mathematical problem of the primitive value.

To verify – Two errors with different origins. Integer.parseInt("twenty") fails because the text does not represent a valid integer. Integer number = null; int value = number; fails because the object from which to extract the value is missing. The two statements do not have the same problem: in the first we check input format, in the second value presence. Write this diagnosis first, then observe the exception types at runtime.

A declared set of values

An enumeration, declared with enum, gives a name and type to a finite set of constants. In the program Status permits NEW and ON_LOAN; Status.ON_LOAN is a value of that type, not a free string subject to typos. We can use it in a switch and, if the domain requires, add fields, constructors and methods to the enumeration. Here allowsLoan() answers true only for NEW: the rule stays close to the values it governs. It should not be chosen for lists changing according to externally loaded data.

enum Status {
    NEW, ON_LOAN;

    boolean allowsLoan() {
        return this == NEW;
    }
}

Enum constants are instances defined by the declaration. They are normally compared with ==, because each constant represents a unique identity in its type. This case does not contradict the criterion explained for our Label objects: they are different models. A constant name can be read with name(), but should not be used as interface text if that description may change or be translated.

From month numbers to a checkable type

Consider twelve integer month constants. An int month variable could also receive 20: the constant name helped write the right value, but did not prevent assigning one outside the list. With enum Month { JANUARY, FEBRUARY, ... }, a Month month variable can contain one of the declared constants or null; an arbitrary integer is not a value of that type. But turning any set of words into an enum is not enough: if the values come from an archive the user can extend, a list fixed in the source does not describe the problem well.

Every enum has values(), returning an array of constants in declaration order. We can traverse it with an enhanced for, as in the month example, but should not save the numeric position obtained from ordinal() in a database: reordering or inserting a constant would change those numbers. An external format requires a stable code decided by the domain. The comparison month == Month.JANUARY is safe even if month is null: it returns false. month.equals(Month.JANUARY), however, would throw NullPointerException in that case.

An enum can have a constructor and fields for data belonging permanently to each constant. In our Status, allowsLoan() is a simple rule associated with the values. A boolean field could distinguish “winter” months: that example needs care because the season also depends on hemisphere and the meteorological or astronomical criterion adopted. A field on each constant can be correct when the classification is an explicit part of the program contract; if it depends on place and date, an operation receiving that context too is needed.

When we encounter collections, EnumSet will give us a specialized set of constants from the same enum, while EnumMap will allow associating a value with each constant. They are not needed for the chapter's first switch: remember only that an enum is a real type, usable by APIs too, not a list of numbers to which we applied more readable names.

An enumeration is more than a set of printable names. If we declare enum Month { JANUARY, FEBRUARY, MARCH } for a reduced experiment, Month.JANUARY has type Month: we cannot directly assign an int or the string "JANUARY" to the variable. To turn text into a constant there is Month.valueOf(text), but it requires an exact name and reports an unknown one with IllegalArgumentException. If the text comes from a person, we must decide how to handle case, spaces and translations before conversion. Type safety in the program does not automatically make external data valid.

When an enum owns data and methods, its constructor is not an invitation to create new constants at runtime. The list remains the declaration's list. For a month we could keep a stable number decided by the domain and offer a number() method, distinguishing it from ordinal(), which simply follows source order. If constant order changes to make code more readable, the domain number must remain what was promised to saved data. This distinction is valuable whenever the program exchanges values with files or databases.

An enum cannot freely extend our own base class, but can implement interfaces and thus respect a contract shared with other types. We will see this later when interfaces have a concrete role in the program. Constant names are often uppercase, for example JANUARY: this is a convention recognizable today too, not a compiler-imposed rule. Choosing stable, readable names matters especially if those names are serialized or read by other components: renaming a constant is then also a change to the external contract.

An enum as a small domain type

Return to a book loan. With a free string we could write "ON_LOAN", "on loan" or "borrowed" and then remember in every method that all three texts mean the same thing. With enum Status { AVAILABLE, ON_LOAN }, the compiler knows the two values permitted by the program. A method canBeLent(Status status) can receive only a value of that type or null; it cannot accidentally receive a String. Checking null remains our decision: the enum type reduces representable states, but does not eliminate the null reference.

Suppose Status offers allowsLoan(). When the book is AVAILABLE, the method returns true; when it is ON_LOAN, false. We have put the rule next to the type describing it, but the book transition still requires an operation: lend() can check the current status and change it. Having an enum does not prevent two program parts from directly modifying a public status field without respecting loan rules. Chapter 8's encapsulation and the enum type solve different problems and work well together.

We can traverse months with an enhanced for, written for (Month month : Month.values()) { ... }: values() returns an array with constants in declaration order. It is useful for building a selection or checking every case, but does not guarantee that order is a stable external-data code. toString() can be overridden to present a description, while name() retains the constant name declared in source. If an interface must show Gennaio in Italian and January in English, translation belongs to the interface or localization resources, not just the uppercase enum value name.

To verify

Compile the complete source with javac --release 25 -Xlint:all -d build DemoValue.java and run java -cp build DemoValue. Predict the seven lines first. Then change only second's text to "JVM": which lines change? Activity C10 also guides a small diagnosis of the contract between equals and hashCode.

Try the examples

Running the programs requires JDK 25. Download the individual Java files linked in the chapter or the complete example project, which includes instructions and a launcher script. The explanations also compare expected output: predict it before running the program.

Massimiliano Tarquini · CC BY-NC 4.0

Back to the top ↑