2.9. Reference Types
Now that we've covered arrays and introduced classes and objects, we can
This section does not cover specific syntax for any particular reference type, but instead explains the general behavior of reference types and illustrates how they
2.9.1. Reference vs. Primitive TypesReference types and objects differ substantially from primitive types and their primitive values:
This last difference between primitive and reference types explains why reference types are so named. The sections that follow are devoted to exploring the substantial differences between types that are manipulated by value and types that are manipulated by reference.
Before moving on, however, it is worth
2.9.2. Copying ObjectsThe following code manipulates a primitive int value: int x = 42; int y = x; After these lines execute, the variable y contains a copy of the value held in the variable x . Inside the Java VM, there are two independent copies of the 32-bit integer 42. Now think about what happens if we run the same basic code but use a reference type instead of a primitive type: Point p = new Point(1.0, 2.0); Point q = p; After this code runs, the variable q holds a copy of the reference held in the variable p . There is still only one copy of the Point object in the VM, but there are now two copies of the reference to that object. This has some important implications. Suppose the two previous lines of code are followed by this code: System.out.println(p.x); // Print out the X coordinate of p: 1.0 q.x = 13.0; // Now change the X coordinate of q System.out.println(p.x); // Print out p.x again; this time it is 13.0
Since the
This behavior is not specific to objects; the same thing happens with arrays, as
char[] greet = { 'h','e','l','l','o' }; // greet holds an array reference
char[] cuss = greet; // cuss holds the same reference
cuss[4] = '!'; // Use reference to change an element
System.out.println(greet); // Prints "hell!"
A similar difference in behavior between primitive types and reference types occurs when arguments are passed to
void changePrimitive(int x) {
while(x > 0)
System.out.println(x--);
}
When this method is invoked, the method is given a copy of the argument used to invoke the method in the parameter
x
. The code in the method uses
x
as a loop counter and decrements it to zero. Since
x
is a primitive type, the method has its own private copy of this value, so this is a
On the other hand, consider what happens if we modify the method so that the parameter is a reference type:
void changeReference(Point p) {
while(p.x > 0)
System.out.println(p.x--);
}
When this method is invoked, it is passed a private copy of a reference to a Point object and can use this reference to change the Point object. Consider the following: Point q = new Point(3.0, 4.5); // A point with an X coordinate of 3 changeReference(q); // Prints 3,2,1 and modifies the Point System.out.println(q.x); // The X coordinate of q is now 0! When the changeReference( ) method is invoked, it is passed a copy of the reference held in variable q . Now both the variable q and the method parameter p hold references to the same object. The method can use its reference to change the contents of the object. Note, however, that it cannot change the contents of the variable q . In other words, the method can change the Point object beyond recognition, but it cannot change the fact that the variable q refers to that object.
The title of this section is "Copying Objects," but, so far, we've only seen copies of references to objects, not copies of the objects and arrays
Point p = new Point(1,2); // p refers to one object
Point q = (Point) p.clone( ); // q refers to a copy of that object
q.y = 42; // Modify the copied object, but not the original
int[] data = {1,2,3,4,5}; // An array
int[] copy = (int[]) data.clone( ); // A copy of the array
Note that a cast is necessary to coerce the return value of the clone( ) method to the correct type. There are a couple of points you should be aware of when using clone( ) . First, not all objects can be cloned. Java only allows an object to be cloned if the object's class has explicitly declared itself to be cloneable by implementing the Cloneable interface. (We haven't discussed interfaces or how they are implemented yet; that is covered in Chapter 3.) The definition of Point that we showed earlier does not actually implement this interface, so our Point type, as implemented, is not cloneable. Note, however, that arrays are always cloneable. If you call the clone( ) method for a noncloneable object, it throws a CloneNotSupportedException . When you use the clone( ) method, you may want to use it within a TRy block to catch this exception.
The second thing you need to understand about
clone( )
is that, by default, it creates a
int[][] data = {{1,2,3}, {4,5}}; // An array of 2 references
int[][] copy = (int[][]) data.clone( ); // Copy the 2 refs to a new array
copy[0][0] = 99; // This changes data[0][0] too!
copy[1] = new int[] {7,8,9}; // This does not change data[1]
If you want to make a deep copy of this multidimensional array, you have to copy each dimension explicitly:
int[][] data = {{1,2,3}, {4,5}}; // An array of 2 references
int[][] copy = new int[data.length][]; // A new array to hold copied arrays
for(int i = 0; i < data.length; i++)
copy[i] = (int[]) data[i].clone( );
2.9.3. Comparing Objects
We've seen that primitive types and reference types differ significantly in the way they are assigned to variables, passed to methods, and copied. The types also differ in the way they are compared for equality. When used with primitive values, the equality operator (
= =
) simply tests whether two values are identical (i.e., whether they have exactly the same bits). With reference types, however,
= =
String letter = "o";
String s = "hello"; // These two String objects
String t = "hell" + letter; // contain exactly the same text.
if (s = = t) System.out.println("equal"); // But they are not equal!
byte[] a = { 1, 2, 3 }; // An array.
byte[] b = (byte[]) a.clone( ); // A copy with identical content.
if (a = = b) System.out.println("equal"); // But they are not equal!
When working with reference types, there are two kinds of equality: equality of reference and equality of object. It is important to distinguish between these two kinds of equality. One way to do this is to use the word "identical" when talking about equality of references and the word "equal" when talking about two distinct objects that have the same content. To test two nonidentical objects for equality, pass one of them to the equals( ) method of the other:
String letter = "o";
String s = "hello"; // These two String objects
String t = "hell" + letter; // contain exactly the same text.
if (s.equals(t)) // And the equals( ) method
System.out.println("equal"); // tells us so.
All objects inherit an equals( ) method (from Object ), but the default implementation simply uses = = to test for identity of references, not equality of content. A class that wants to allow objects to be compared for equality can define its own version of the equals( ) method. Our Point class does not do this, but the String class does, as indicated in the code example. You can call the equals( ) method on an array, but it is the same as using the = = operator, because arrays always inherit the default equals( ) method that compares references rather than array content. You can compare arrays for equality with the convenience method java.util.Arrays.equals( ) . 2.9.4. Terminology: Pass by Value
I've said that Java handles objects "by reference." Don't confuse this with the phrase "pass by reference." "Pass by reference" is a term used to describe the method-calling conventions of some programming languages. In a
Java does not do this; it is a "pass by value" language. However, when a reference type is involved, the value that is passed is a reference. But this is still not the same as pass-by-reference. If Java were a pass-by-reference language, when a reference type is passed to a method, it would be passed as a reference to the reference. 2.9.5. Memory Allocation and Garbage CollectionAs we've already noted, objects are composite values that can contain a number of other values and may require a substantial amount of memory. When you use the new keyword to create a new object or use an object literal in your program, Java automatically creates the object for you, allocating whatever amount of memory is necessary. You don't need to do anything to make this happen.
In addition, Java also automatically reclaims that memory for reuse when it is no longer needed. It does this through a process called
garbage collection
. An object is
Point p = new Point(1,2); // Create an object double d = p.distanceFromOrigin( ); // Use it for something p = new Point(2,3); // Create a new object
After the Java interpreter executes the third line, a reference to the new
Point
object has
C programmers, who are used to using
malloc( )
and
free( )
to manage memory, and C++ programmers, who are used to explicitly deleting their objects with
delete
, may find it a little hard to relinquish control and trust the garbage collector. Even though it seems like magic, it really works! There is a
2.9.6. Reference Type ConversionsObjects can be converted between different reference types. As with primitive types, reference type conversions can be widening conversions (allowed automatically by the compiler) or narrowing conversions that require a cast (and possibly a runtime check). In order to understand reference type conversions, you need to understand that reference types form a hierarchy, usually called the class hierarchy . Every Java reference type extends some other type, known as its superclass . A type inherits the fields and methods of its superclass and then defines its own additional fields and methods. A special class named Object serves as the root of the class hierarchy in Java. All Java classes extend Object directly or indirectly. The Object class defines a number of special methods that are inherited (or overridden) by all objects.
The predefined
String
class and the
Point
class we discussed earlier in this chapter both extend
Object
. Thus, we can say that all
String
objects are also
Object
objects. We can also say that all
Point
objects are
Object
objects. The
With this simple understanding of the class hierarchy, we can return to the rules of reference type conversion:
Object o = "string"; // Widening conversion from String to Object // Later in the program... String s = (String) o; // Narrowing conversion from Object to String Arrays are objects and follow some conversion rules of their own. First, any array can be converted to an Object value through a widening conversion. A narrowing conversion with a cast can convert such an object value back to an array. For example:
Object o = new int[] {1,2,3}; // Widening conversion from array to Object
// Later in the program...
int[] a = (int[]) o; // Narrowing conversion back to array type
In addition to converting an array to an object, an array can be converted to another type of array if the "base types" of the two arrays are reference types that can themselves be converted. For example:
// Here is an array of strings.
String[] strings = new String[] { "hi", "there" };
// A widening conversion to CharSequence[] is allowed because String
// can be widened to CharSequence
CharSequence[] sequences = strings;
// The narrowing conversion back to String[] requires a cast.
strings = (String[]) sequences;
// This is an array of arrays of strings
String[][] s = new String[][] { strings };
// It cannot be converted to CharSequence[] because String[] cannot be
// converted to CharSequence: the number of dimensions don't match
sequences = s; // This line will not compile
// s can be converted to Object or Object[], however because all array types
// (including String[] and String[][]) can be converted to Object.
Object[] objects = s;
Note that these array conversion rules apply only to arrays of objects and arrays of arrays. An array of primitive type cannot be converted to any other array type, even if the primitive base types can be converted:
// Can't convert int[] to double[] even though int can be widened to double
double[] data = new int[] {1,2,3}; // This line causes a compilation error
// This line is legal, however, since int[] can be converted to Object
Object[] objects = new int[][] {{1,2},{3,4}};
2.9.7. Boxing and Unboxing ConversionsPrimitive types and reference types behave quite differently. It is sometimes useful to treat primitive values as objects, and for this reason, the Java platform includes wrapper classes for each of the primitive types. Boolean , Byte , Short , Character , Integer , Long , Float , and Double are immutable classes whose instances each hold a single primitive value. These wrapper classes are usually used when you want to store primitive values in collections such as java.util.List : List numbers = new ArrayList( ); // Create a List collection numbers.add(new Integer(-1)); // Store a wrapped primitive int i = ((Integer)numbers.get(0)).intValue( ); // Extract the primitive value Prior to Java 5.0, no conversions between primitive types and reference types were allowed. This code explicitly calls the Integer( ) constructor to wrap a primitive int in an object and explicitly calls the intValue( ) method to extract a primitive value from the wrapper object. Java 5.0 introduces two new types of conversions known as boxing and unboxing conversions. Boxing conversions convert a primitive value to its corresponding wrapper object and unboxing conversions do the opposite. You may explicitly specify a boxing or unboxing conversion with a cast, but this is unnecessary since these conversions are automatically performed when you assign a value to a variable or pass a value to a method. Furthermore, unboxing conversions are also automatic if you use a wrapper object when a Java operator or statement expects a primitive value. Because Java 5.0 performs boxing and unboxing automatically, this new language feature is often known as autoboxing . Here are some examples of automatic boxing and unboxing conversions: Integer i = 0; // int literal 0 is boxed into an Integer object Number n = 0.0f; // float literal is boxed into Float and widened to Number Integer i = 1; // this is a boxing conversion int j = i; // i is unboxed here i++; // i is unboxed, incremented, and then boxed up again Integer k = i+2; // i is unboxed and the sum is boxed up again i = null; j = i; // unboxing here throws a NullPointerException
Automatic boxing and unboxing conversions make it much simple to use primitive values with collection classes. The list-of-numbers code earlier in this section can be translated as
List<Integer> numbers = new ArrayList<Integer>( ); // Create a List of Integer numbers.add(-1); // Box int to Integer int i = numbers.get(0); // Unbox Integer to int |