Java Language Pitfall: thinking of variables as objects


Example

No Java variable represents an object.

String foo;   // NOT AN OBJECT

Neither does any Java array contain objects.

String bar[] = new String[100];  // No member is an object.

If you mistakenly think of variables as objects, the actual behavior of the Java language will surprise you.

  • For Java variables which have a primitive type (such as int or float) the variable holds a copy of the value. All copies of a primitive value are indistinguishable; i.e. there is only one int value for the number one. Primitive values are not objects and they do not behave like objects.

  • For Java variables which have a reference type (either a class or an array type) the variable holds a reference. All copies of a reference are indistinguishable. References may point to objects, or they may be null which means that they point to no object. However, they are not objects and they don't behave like objects.

Variables are not objects in either case, and they don't contain objects in either case. They may contain references to objects, but that is saying something different.

Example class

The examples that follow use this class, which represents a point in 2D space.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

An instance of this class is an object that has two fields x and y which have the type int.

We can have many instances of the MutableLocation class. Some will represent the same locations in 2D space; i.e. the respective values of x and y will match. Others will represent different locations.

Multiple variables can point to the same object

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

In the above, we have declared three variables here, there and elsewhere that can hold references to MutableLocation objects.

If you (incorrectly) think of these variables as being objects, then you are likely to misread the statements as saying:

  1. Copy the location "[1, 2]" to here
  2. Copy the location "[1, 2]" to there
  3. Copy the location "[1, 2]" to elsewhere

From that, you are likely to infer we have three independent objects in the three variables. In fact there are only two objects created by the above. The variables here and there actually refer to the same object.

We can demonstrate this. Assuming the variable declarations as above:

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

This will output the following:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

We assigned a new value to here.x and it changed the value that we see via there.x. They are referring to the same object. But the value that we see via elsewhere.x has not changed, so elsewhere must refer to a different object.

If a variable was an object, then the assignment here.x = 42 would not change there.x.

The equality operator does NOT test that two objects are equal

Applying the equality (==) operator to reference values tests if the values refer to the same object. It does not test whether two (different) objects are "equal" in the intuitive sense.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

This will print "here is there", but it won't print "here is elsewhere". (The references in here and elsewhere are for two distinct objects.)

By contrast, if we call the equals(Object) method that we implemented above, we are going to test if two MutableLocation instances have an equal location.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

This will print both messages. In particular, here.equals(elsewhere) returns true because the semantic criteria we chose for equality of two MutableLocation objects has been satisfied.

Method calls do NOT pass objects at all

Java method calls use pass by value1 to pass arguments and return a result.

When you pass a reference value to a method, you're actually passing a reference to an object by value, which means that it is creating a copy of the object reference.

As long as both object references are still pointing to the same object, you can modify that object from either reference, and this is what causes confusion for some.

However, you are not passing an object by reference2. The distinction is that if the object reference copy is modified to point to another object, the original object reference will still point to the original object.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Neither are you passing a copy of the object.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - In languages like Python and Ruby, the term "pass by sharing" is preferred for "pass by value" of an object / reference.

2 - The term "pass by reference" or "call by reference" has a very specific meaning in programming language terminology. In effect, it means that you pass the address of a variable or an array element, so that when the called method assigns a new value to the formal argument, it changes the value in the original variable. Java does not support this. For a more fulsome description of different mechanisms for passing parameters, please refer to https://en.wikipedia.org/wiki/Evaluation_strategy.