Chapter 4. Reference Types

Reference types hold references to objects and provide a means to access those objects stored somewhere in memory. The memory locations are irrelevant to programmers. All reference types are a subclass of type java.lang.Object.

Table 4-1 lists the five Java reference types.

Table 4-1. Reference types
Reference type Brief description

Annotation

Provides a way to associate metadata (data about data) with program elements.

Array

Provides a fixed-size data structure that stores data elements of the same type.

Class

Designed to provide inheritance, polymorphism, and encapsulation. Usually models something in the real world and consists of a set of values that holds data and a set of methods that operates on the data.

Enumeration

A reference for a set of objects that represents a related set of choices.

Interface

Provides a public API and is “implemented” by Java classes.

Comparing Reference Types to Primitive Types

There are two type categories in Java: reference types and primitive types. Table 4-2 shows some of the key differences between them. See Chapter 3 for more details.

Table 4-2. Reference types compared with primitive types
Reference types Primitive types

Unlimited number of reference types, as they are defined by the user.

Consists of boolean and numeric types: char, byte, short, int, long, float, and double.

Memory location stores a reference to the data.

Memory location stores actual data held by the primitive type.

When a reference type is assigned to another reference type, both will point to the same object.

When a value of a primitive is assigned to another variable of the same type, a copy is made.

When an object is passed into a method, the called method can change the contents of the object passed to it but not the address of the object.

When a primitive is passed into a method, only a copy of the primitive is passed. The called method does not have access to the original primitive value and therefore cannot change it. The called method can change the copied value.

Default Values

Default values are the values assigned to instance variables in Java, when no initialization value has been explicitly set.

Instance and Local Variable Objects

Instance variables (i.e., those declared at the class level) have a default value of null. null references nothing.

Local variables (i.e., those declared within a method) do not have a default value, not even a value of null. Always initialize local variables because they are not given a default value. Checking an uninitialized local variable object for a value (including a value of null) will result in a compile-time error.

Although object references with a value of null do not refer to any object on the heap, objects set to null can be referenced in code without receiving compile-time or runtime errors:

LocalDate birthdate = null;
// This will compile
if (birthdate == null) {
  System.out.println(birthdate);
}
$ null

Invoking a method on a reference variable that is null or using the dot operator on the object will result in a java.lang.NullPointerException:

final int MAX_LENGTH = 20;
String partyTheme = null;
/*
 * java.lang.NullPointerException is thrown
 * since partyTheme is null
 */
if (partyTheme.length() > MAX_LENGTH) {}

Arrays

Arrays are always given a default value whether they are declared as instance variables or local variables. Arrays that are declared but not initialized are given a default value of null.

In the following code, the gameList1 array is initialized, but not the individual values, meaning that the object references will have a value of null. Objects have to be added to the array:

/*
 * The declared arrays named gameList1 and
 * gameList2 are initialized to null by default
  */
Game[] gameList1;
Game gameList2[];

/*
 * The following array has been initialized but
 * the object references are still null because
 * the array contains no objects
 */
   gameList1 = new Game[10];

// Add a Game object to the list, so it has one object
   gameList1[0] = new Game();

Multidimensional arrays in Java are actually arrays of arrays. They may be initialized with the new operator or by placing their values within braces. Multidimensional arrays may be uniform or nonuniform in shape:

// Anonymous array
int twoDimensionalArray[][] = new int[6][6];
twoDimensionalArray[0][0] = 100;
int threeDimensionalArray[][][] = new int[2][2][2];
threeDimensionalArray[0][0][0] = 200;
int varDimensionArray[][] = {{0,0},{1,1,1},
{2,2,2,2}};
varDimensionArray[0][0] = 300;

Anonymous arrays allow for the creation of a new array of values anywhere in the code base:

// Examples using anonymous arrays
int[] luckyNumbers = new int[] {7, 13, 21};
int totalWinnings = sum(new int[] {3000, 4500,
5000});

Conversion of Reference Types

An object can be converted to the type of its superclass (widening) or any of its subclasses (narrowing).

The compiler checks conversions at compile time, and the Java Virtual Machine (JVM) checks conversions at runtime.

Widening Conversions

  • Widening implicitly converts a subclass to a parent class (superclass).

  • Widening conversions do not throw runtime exceptions.

  • No explicit cast is necessary:

    String s = new String();
    Object o = s; // widening

Narrowing Conversions

  • Narrowing converts a more general type into a more specific type.

  • Narrowing is a conversion of a superclass to a subclass.

  • An explicit cast is required. To cast an object to another object, place the type of object to which you are casting in parentheses immediately before the object you are casting.

  • Illegitimate narrowing results in a ClassCastException.

  • Narrowing may result in a loss of data/precision.

Objects cannot be converted to an unrelated type—that is, a type other than one of its subclasses or superclasses. Doing so will generate an inconvertible types error at compile time. The following is an example of a conversion that will result in a compile-time error due to inconvertible types:

Object o = new Object();
String s = (Integer) o;  // compile-time error

Converting Between Primitives and Reference Types

The automatic conversion of primitive types to reference types, and vice versa, is called autoboxing and unboxing, respectively. For more information, refer back to Chapter 3.

Passing Reference Types into Methods

When an object is passed into a method as a variable:

  • A copy of the reference variable is passed, not the actual object.

  • The caller and the called methods have identical copies of the reference.

  • The caller will also see any changes the called method makes to the object. Passing a copy of the object to the called method will prevent it from making changes to the original object.

  • The called method cannot change the address of the object, but it can change the contents of the object.

The following example illustrates passing reference types and primitive types into methods and the effects on those types when changed by the called method:

void roomSetup() {
  // Reference passing
  Table table = new Table();
  table.setLength(72);
  // Length will be changed
  modTableLength(table);

  // Primitive passing
  // Value of chairs not changed
  int chairs = 8;
  modChairCount(chairs);
}

void modTableLength(Table t) {
  t.setLength(36);
}

void modChairCount(int i) {
  i = 10;
}

Comparing Reference Types

Reference types are comparable in Java. Equality operators and the equals method can be used to assist with comparisons.

Using the Equality Operators

The != and == equality operators are used to compare the memory locations of two objects. If the memory addresses of the objects being compared are the same, the objects are considered equal. These equality operators are not used to compare the contents of two objects.

In the following example, guest1 and guest2 have the same memory address, so the statement "They are equal" is output:

String guest1 = new String("name");
String guest2 = guest1;
if (guest1 == guest2)
  System.out.println("They are equal");

In the following example, the memory addresses are not equal, so the statement "They are not equal" is output:

String guest1 = new String("name");
String guest2 = new String("name");
if (guest1 != guest2)
  System.out.println("They are not equal");

Using the equals() Method

To compare the contents of two class objects, the equals()method from class Object can be used or overridden. When the equals() method is overridden, the hashCode() method should also be overridden. This is done for compatibility with hash-based collections such as HashMap() and HashSet().

Tip

By default, the equals() method uses only the == operator for comparisons. This method has to be overridden to really be useful.

For example, if you want to compare values contained in two instances of the same class, you should use a programmer-defined equals() method.

Comparing Strings

There are two ways to check whether strings are equal in Java, but the definition of “equal” for each of them is different:

  • The equals() method compares two strings, character by character, to determine equality. This is not the default implementation of the equals() method provided by the Object class. This is the overridden implementation provided by String class.

  • The == operator checks to see whether two object references refer to the same instance of an object.

Here is a program that shows how strings are evaluated using the equals() method and the == operator (for more information on how strings are evaluated, see “String Literals” in Chapter 2):

class MyComparisons {

  // Add string to pool
  String first = "chairs";
  // Use string from pool
  String second = "chairs";
  // Create a new string
  String third = new String ("chairs");

 void myMethod() {

  /*
   * Contrary to popular belief, this evaluates
   * to true. Try it!
   */
  if (first == second) {
    System.out.println("first == second");
  }

  // This evaluates to true
  if (first.equals(second)) {
    System.out.println("first equals second");
  }
  // This evaluates to false
  if (first == third) {
    System.out.println("first == third");
  }
  // This evaluates to true
  if (first.equals(third)) {
    System.out.println("first equals third");
  }
 } // End myMethod()
} //end class
Tip

Objects of the StringBuffer and StringBuilder classes are mutable. Objects of the String class are immutable.

Comparing Enumerations

enum values can be compared using == or the equals() method because they return the same result. The == operator is used more frequently to compare enumeration types.

Copying Reference Types

When reference types are copied, either a copy of the reference to an object is made, or an actual copy of the object is made, creating a new object. The latter is referred to as cloning in Java.

Copying a Reference to an Object

When copying a reference to an object, the result is one object with two references. In the following example, closingSong is assigned a reference to the object pointed to by lastSong. Any changes made to lastSong will be reflected in closingSong, and vice versa:

Song lastSong = new Song();
Song closingSong = lastSong;

Cloning Objects

Cloning results in another copy of the object, not just a copy of a reference to an object. Cloning is not available to classes by default. Note that cloning is usually very complex, so you should consider a copy constructor instead, for the following reasons:

  • For a class to be cloneable, it must implement the interface Cloneable.

  • The protected method clone() allows for objects to clone themselves.

  • For an object to clone an object other than itself, the clone() method must be overridden and made public by the object being cloned.

  • When cloning, a cast must be used because clone() returns type object.

  • Cloning can throw a CloneNotSupportedException.

Shallow and deep cloning

Shallow and deep cloning are the two types of cloning in Java.

In shallow cloning, primitive values and the references in the object being cloned are copied. Copies of the objects referred to by those references are not made.

In the following example, leadingSong will be assigned the value of length because it is a primitive type. Also, leadingSong will be assigned the references to title, artist, and year because they are references to types:

Class Song {
  String title;
  Artist artist;
  float length;
  Year year;
  void setData() {...}
}
Song firstSong = new Song();
try  {
  // Make an actual copy by cloning
  Song leadingSong = (Song)firstSong.clone();
} catch (CloneNotSupportedException cnse) {
  cnse.printStackTrace();
} // end

In deep cloning, the cloned object makes a copy of each of its object’s fields, recursing through all other objects referenced by it. A deep-clone method must be defined by the programmer, as the Java API does not provide one. Alternatives to deep cloning are serialization and copy constructors. (Copy constructors are often preferred over serialization.)

Memory Allocation and Garbage Collection of Reference Types

When a new object is created, memory is allocated. When there are no references to an object, the memory that object used can be reclaimed during the garbage collection process. For more information on this topic, see Chapter 11.

Get Java Pocket Guide, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.