CMPU 102, Fall 2005 Lecture 2

Today's lecture will review Java objects, classes, and references, and begin reviewing arrays.

Outline

Switch statements

A switch statement is like a chain of if/else statements that test a single variable against a series of constant values:

char c = getChar();

switch (c) {
case 'A':
    System.out.println("c == A");
    break;

case 'B':
    System.out.println("c == B");
    break;

default:
    System.out.println("c is not A or B");
    break;
}

The following code is equivalent:

char c = getChar();

if (c == 'A') {
    System.out.println("c == A");
} else if (c == 'B') {
    System.out.println("c == B");
} else {
    System.out.println("c is not A or B");
}

You can have multiple case values which target the same block of code:

char c = getChar();

switch (c) {
case 'A':
case 'B':
case 'C':
    System.out.println("c is A, B, or C");
    break;

case 'D':
    System.out.println("c == D");
    break;

default:
    System.out.println("C is not A, B, C, or D");
    break;
}

If you omit a "break" statement, then the code will fall through to the next statement:

char c = getChar();

switch (c) {
case 'A':
    System.out.println("c is A");
    // Fall through!

case 'B':
    System.out.println("c is A or B");
    break;
}

If c has the value 'A', the code above will print:

c is A
c is A or B

Experience has shown that fall through is far more likely to be a mistake---forgetting a break statement---than it is to be intentional.  Therefore, you should make sure that all of your switch cases end with a break statement.

Memory cells

Todays lecture will talk about aggregate data types: objects and arrays.  These are data types composed of simpler data types.  In order to understand aggregates, it is important to understand what their fundamental building blocks are.  One way to think about aggregates is that they are bundles of memory cells.

You can think of a memory cell as a box that can hold a scalar value of a particular type.  The type of the value stored in the box can be a primitive type, such as int, char, float, double, and the other primitive types we reviewed in the last lecture, or it can be a reference to an object or array.  Here are two examples: a memory cell holding the int value 4 and a memory cell holding a reference to the String "Hello, world":

Memory cells are generally mutable, meaning that you can store a new value in the box as long as the new value has a type that is compatible with the type of the memory cell.  For example, you can store the value 42 in a memory cell specified to hold values of type int.  However, you cannot store a reference to a java.lang.String object in a memory cell declared to hold int values.

Each memory cell is distinct: storing a value in one memory cell will not change the value of any other memory cell.

You can always read the current value from any memory cell, and you are guaranteed to get a value compatible with the type of the memory cell.  For example, if you read the value of an int memory cell, you are guaranteed to get an integer (and not a java.lang.String reference, a double, etc.).

A local variable in a method is a memory cell.  The fields of an object are also memory cells, as are the elements of an array.

Objects and classes

An object is a bundle of fields (memory cells) and methods.  Every object is an instance of a class, which defines the object's fields and methods.  Just like local variables, each field of a class, and thus the objects that are instances of that class, has a name.

From a design standpoint, an object represents some entity in the problem domain.  For example, let's say the problem domain is a children's game where some of the characters are dogs.  You could write a class called "Dog", where instances of this class would represent particular dogs in the game.

/**
 * A simple Java class representing a Dog.
 */
public class Dog {
  private String name;
  private boolean goodDog;

  public Dog(String name, boolean goodDog) {
    this.name = name;
    this.goodDog = goodDog;
  }

  public void bark() {
    System.out.println(name + " barks");
  }

  public void respondToCall(String nameCalled) {
    // Good dogs come when their name is called

    if (name.equals(nameCalled) && goodDog) {
      System.out.println(name + " comes");
    } else {
      System.out.println(name + " does not respond");
    }
  }

  public boolean isGoodDog() {
    return goodDog;
  }

  public void train() {
    goodDog = true;
  }
}

Fields and Constructors

The fields of an object store the state of the object.  In the example, the two fields in the Dog class represent the dog's name, and whether or not the dog is a good dog.

A class generally defines one or more constructors which define how new instances of the class are created.  A constructor generally assigns each field of the new object an initial value, often by assigning the value of one of the constructor's parameters to the field.  Note that if a parameter has the same name as a field, then you must write "this.fieldname" to refer to the field---otherwise, the parameter takes precedence.

In the example above, we've defined a single constructor that takes two parameters (a String and a boolean) and uses them to initialize the two fields of the object.

As an example, we can create two Dog objects:

Dog lassie = new Dog("Lassie", true);
Dog rex = new Dog("Rex", false);

Recall that each field of an object is a memory cell, and so are local variables.  Here is what memory will look like once the objects have been created:

References

A reference is an arrow pointing to a particular object or array:

In Java, the only way to access an object or array is through a reference.  When you declare a local variable or field which has a class or array type, what you are really doing is defining a memory cell that can hold a reference to that kind of class or array.

The important thing to understand about references is that copying a reference does not copy the object.  For example:

Dog rover1, rover2;

rover1 = new Dog("Rover", false);
rover2 = rover1; // Only the reference is copied, not the object!

After this code is executed, both references point to the same object:

We can see this clearly by modifying the object through one reference, and observing the change is reflected in the other reference.

rover1.respondToCall("Rover"); // "Rover does not respond"
rover2.respondToCall("Rover"); // "Rover does not respond"

// rover1 and rover2 both point to the same object, so any
// changes we make to the object are reflected by both references
rover1.train();
rover1.respondToCall("Rover"); // "Rover comes"
rover2.respondToCall("Rover"); // "Rover comes"

The equality and inequality operators ("==" and "!=") are defined for references.  These will tell you whether or not two references point to the same object.  However, they will not tell you anything about the contents of the objects (or arrays) themselves!  This can be seen from the following code:

Dog spot = new Dog("Spot", true);
Dog anotherSpot = new Dog("Spot", true);

if (spot == anotherSpot) {
  System.out.println("same");
} else {
  System.out.println("different");
}

The code above prints "different", because "spot" and "anotherSpot" are two different objects, even though the contents of those objects are exactly the same.

To compare the contents of two objects to see if they are the same, you should use the equals method, as seen in the definition of the "respondToCall" method.  Using "==" or "!=" to compare objects instead of the equals method is a very common bug in Java programs, even for experienced programmers.

Null

The special reference null represents the absence of any object or array.  Basically, it means "points to nothing".  If you try to use a null value---for example, by invoking a method on it---a NullPointerException will be thrown.  In general, this only happens if you have a bug in your program.

Null references are generally drawn like this:

Recursive data types

The fields of a class can have any valid type.  This means that an object may have one or more fields that have the same type as the object itself!  Classes that define fields of the same type as the class itself are recursive data types.  Many important data structures, such as linked lists and trees, are recursive data types.

Methods

The instance methods of a class perform operations on instances of the class.  In our Dog class, we defined four instance methods: "bark", "respondToCall", "isGoodDog", and "train".  In an instance method, the value "this" is a reference pointing to the object on which the method was invoked.  You can use "this" to explicitly refer to fields and instance methods of the object.  However, in most cases, you can omit "this", because by default, if you refer to a field or instance method, it is assumed that you mean the one belonging to the "this" object.

Static methods are methods which are not invoked on a particular object.  Therefore, you cannot refer to "this" in a static method.  Static methods are sometimes useful, but in order to appreciate the benefits of working in an object-oriented language, we will generally avoid using them.

The dot operator

To access a field or method of an object through a reference to that object, you use the dot operator:

Dog fifi = new Dog("Fifi", true);
fifi.bark();   // Prints "Fifi barks"

if (fifi.isGoodDog()) {
  System.out.println("Fifi is a good dog");
}

Arrays

Objects are useful when you have a fixed number of data values.  However, in some cases, you will need to store an arbitrary number of data values: for this purpose, you can use an array.  An array stores a fixed number of values, all of the same type.  For example, this code creates an array of 10 int values, and initializes them with the values 0 through 9:

int[] arrayOfInts = new int[10];
for (int i = 0; i < arrayOfInts.length; i++) {
  arrayOfInts[i] = i;
}

As seen in the example above, arrays have a "length" field indicating how many elements the array contains.

To assign or refer to a particular element of an array, use the subscript operator:

System.out.println("Element 6 contains " + arrayOfInts[6]);

The value you use as the subscript must be an integer within the range 0 to array.length - 1.  If you pass a negative subscript, or a subscript that is greater than or equal to the length of the array, an ArrayIndexOutOfBoundsException will be thrown.

Like ordinary objects, arrays are accessed through references.

We will discuss arrays in more detail in the next lecture.