Lecture 6

In this lecture we will explore the semantics of objects, references, and aliasing.  These concepts are fundamental to the way that Java works, so understanding them is very important.

Mutating an Object

Consider the Person class from the previous lecture:

public class Person {
  private String name;
  private int age;
  private String hobby;

  public Person(String name, int age, String hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
  }

  public String getName() { return this.name; }
  public int getAge() { return this.age; }
  public String getHobby() { return this.hobby; }
}

This is an immutable class because once instances (objects) of the class are created, the value of their instance variables never change.  Some classes are designed to be immutable, such as the java.lang.String class used to represent strings of characters.  However, often we want objects to be able to change as the program runs.  For example, our Person class has an age instance variable.  In real life, people get older, so it makes sense to allow the value stored in the age variable to change.  Let's add a setter method that will allow us to change the value of this variable when it is invoked:

public class Person {
  private String name;
  private int age;
  private String hobby;

  public Person(String name, int age, String hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
  }

  public String getName() { return this.name; }
  public int getAge() { return this.age; }
  public String getHobby() { return this.hobby; }

  public void setAge(int age) {
    this.age = age;
  }
}

The setAge method takes an int value as a parameter and stores the value in the age instance variable of the object on which the method is invoked (this).  Let's observe this in the interactions window:

Conceptually, here is what happens.  Before the call to setAge, the variable alice and the Person object it refers to look like this:

After the call to setAge, they look like this:

Aliasing

Try typing the following statements and expressions in the DrJava interactions window:

int x = 42;
int y = x;
x
y
x = 17;
x
y

We are:

  1. declaring a variable x and initializing it to the value 42,
  2. declaring another variable x and initializing it to y's value
  3. inspecting x and y and finding that, unsurprisingly, they both contain the value 42
  4. Next, assigning x the value 17
  5. inspecting x and y again

After the last assignment, x has been updated to the new value 17, but y retains its original value, 42.  This is because copying one variable to another only copies the value: they remain distinct memory locations, and their values may be changed independently.

Now let's try a similar example, but rather than using int variables, using Person variables:

Person person1 = new Person("Carl", 31, "sudoku");
Person person2 = person1;
person1.getAge();
person2.getAge();
person1.setAge(32);
person2.getAge();

We are:

  1. Declaring a variable person1 and initializing it with a reference to a new Person object
  2. Declaring a variable person2 and initializing it with the value of person1
  3. Calling getAge on both person1 and person2 and finding that they both return 31
  4. Calling setAge on person1, passing the value 32
  5. Calling getAge again on person2, and finding that its value has changed!

Here is the actual interactions window:

So, what happened here?  We invoked a setter method on person1, and it somehow modified person2 as well!

The critical step in this example was this statement:

Person person2 = person1;

On the surface, it looks like we are creating a new person object, person2, that is copied from an existing Person object, person1.  However, what is actually happening is that the assignment of person1's value to person2 is only copying a reference.  There is still only a single Person object, but now it has two names.  Here is a picture of the situation:

Small Boxes, Large Boxes, and Arrows

Here is a graphical notation that will help you understand the relationship between variables, references, and objects.

A variable is a small box.  It can hold a primitive value or a reference to an object.  It also has a name.

 

An object is a large box: too large to fit inside a variable.  Each object is an instance of a particular class (such as Person).  It contains instance variables: these are just normal variables (small boxes) that are stored inside the object.

 

A reference is an arrow.  The arrow begins inside a variable whose type is a class, and points to an object that is an instance of that class.

When you copy a reference from one variable to another, as we did in the assignment person2 = person1, we are making the destination variable (person2) point to whatever object the assigned value (person1) referred to.

Get in the habit of drawing these diagrams when you are working with classes, variables, and objects.

The toString method

A common task in implementing classes is converting instances of the class to Strings.  This is accomplished by adding a method called toString to the class.  This method must not be static, must return a value of type String, and must not have any parameters.  Let's add one to the Person class:

public class Person {
  private String name;
  private int age;
  private String hobby;

  public Person(String name, int age, String hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
  }

  public String getName() { return this.name; }
  public int getAge() { return this.age; }
  public String getHobby() { return this.hobby; }

  public void setAge(int age) {
    this.age = age;
  }

  public String toString() {
    String description;
    description = "Name: " + this.name + ", Age: " + this.age + ", Hobby: " + this.hobby;
    return description;
  }
}

In the implementation of the toString method, we use a string concatenation expression to combine the values of several literal string constants and the values of the object's instance variables.  The resulting String value is then returned as the method's return value.

toString is useful when called directly on an object, but its main purpose is to define a way to convert objects to Strings when they appear in string concatenation expressions.  We can see this in the DrJava interactions window:

In the string concatenation expression

"details about Alice - " + alice

the Java compiler automatically uses the toString method to convert the Person object referred to by the variable alice into a String.  The resulting value is then concatenated with a literal String constant, yielding the result of the overall expression.

SmileyFace class

Let's consider another class, SmileyFace.  The purpose of this class is to draw a simley face in a GUI window.  By creating instances of this class, we can easily draw and animate any number of smiley faces of various sizes and colors.

We will have four instance variables: x, y, radius, and colorx and y are the x and y coordinates defining the center of the smiley face.  radius specifies how large the smiley face will be.  color defines the color we will use to draw the smiley face.

Aside from the constructor used to initialize new SmileyFace instances, the class will define only one method, called draw.  The purpose of this method is to draw the smiley face (using the position, size, and color specified by the object's instance variables) using the methods of a java.awt.Graphics object passed as a parameter.

Here is the complete class definition:

import java.awt.Color;
import java.awt.Graphics;

public class SmileyFace {
  // Where is the smiley face?
  private int x;
  private int y;
  // How large is it?
  private int radius;
  // What color is it?
  private Color color;
  
  public SmileyFace(int x, int y, int radius, Color color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }
  
  public void draw(Graphics graphics) {
    graphics.setColor(this.color);

    // Draw the circle
    graphics.drawOval(this.x - radius, this.y - radius, this.radius * 2, this.radius * 2);
    
    // Draw filled circles for the eyes
    int halfRadius = this.radius / 2;
    graphics.fillOval(this.x - halfRadius, this.y - halfRadius, 6, 6);
    graphics.fillOval(this.x + halfRadius, this.y - halfRadius, 6, 6);
    
    // Draw the mouth
    graphics.drawArc(this.x - halfRadius, this.y, this.radius, halfRadius, 180, 180);
  }
}

The smiley face is drawn using the setColor, drawOval, fillOval, and drawArc methods of the Graphics class.  The setColor method sets the "pen color" that will be used for drawing.  The argument values used for the methods that actually perform the drawing are computed as functions of the values of the instance variables x, y, and radius.  This allows the drawing to drawn at any position using any size.  Here is a window in which two SmileyFace objects have drawn themselves:

You can try running this program: download SmileyWindow.java and SmileyFace.java, load and compile them in DrJava, and run the command

java SmileyWindow

in the interactions window.