Lecture 9: Classes and objects

Objectives

  • Construct, use, and pass objects as arguments
  • Use null references appropriately
  • Write classes and use objects

In the last lecture, we discussed how Java handles memory and scope between variables, methods, and fields (variables declared in a class outside of a method). In this lecture, we’ll (finally!) go over classes and show how fields are used to represent object state. This is the last lecture of the semester.

Why are classes and objects useful in computation?

  • Large, complex projects worked on by many people in parallel
  • Ability to reuse code others have written, by extending it and making it your own
  • Other examples?

Classes vs objects

We have been defining lots of classes all semester – in fact, to be able to run any code in Java, we need a main method inside of some arbitrary class! However, we were never really using classes so far for their intended purpose: to group related data and methods about some user-defined type (the class) in one place.

An example class: our own Strings

We saw how Java has a String class that likely uses some array-like structure to store a sequence of characters, and then provides hundreds of methods to operate on this data. Imagine, however, no one had defined such a class. Let’s define our own FakeString class to illustrate how they might have done this.

You probably ended up with something like this:

public class FakeString {

}  

You probably ended up with something like this:

public class FakeString {
    char[] letters = new char[255];
    int actualLength = 0;
}  

You probably ended up with something like this:

public class FakeString {
    char[] letters = new char[255];
    int actualLength = 0;

    public void addLetter(char letter){
        letters[actualLength] = letter;
        actualLength++;
    }

    public char charAt(int i){
        return letters[i];
    }

    public boolean isFull(){
        return (letters.length == acutalLength);
    }
}  

This is all very useful, but how do we actually create a FakeString object and call the methods above on it?

Constructors

In Java, classes are a blueprint for what their respective objects look like. We need a way actually create these objects, and we do that with a special method called a constructor:

public class FakeString {
    char[] letters = new char[255];
    int actualLength = 0;

    public FakeString(char[] lettersIn){
        for(int i = 0; i < lettersIn.length; i++)
            addLetter(lettersIn[i]);
    }

    public FakeString(int length){
        letters = new char[length];
    }

    ...
}  

A constructor has the same name as the class and no return type, and it’s job is to create a default object on the heap using the fields. It keeps track of the memory address of this new object and uses the new keyword to reserve space on the heap for this new object.

Above, we see two constructors – the compiler can distinguish between them because their arguments are different. We can now use these constructors to generate FakeString objects, perhaps in another class:

public class Driver {
    public static void main(String[] args){
        char[] purple = {'p', 'u', 'p', 'l', 'e'};
        FakeString color1 = new FakeString(purple);

        FakeString color2 = new FakeString(10);

        color1.addLetter('Q');
        color2.addLetter('Q');
    }
}  

Once we have created the color1 and color2 objects, we can then call methods on them like addLetter, using the dot operator.

Class Participation Activity 1 [5 min]

Let’s formally trace the first example above, paying attention to how the two objects are created on the heap.

Default constructors and default values

It turns out that even if we didn’t write those two constructors, Java would have given us a default constructor for free:

public class Driver {
    public static void main(String[] args){

        FakeString colorDefault = new FakeString();
    }
}  

The default constructor takes no arguments, and initializes all fields to their default values. But, what is a default value?

It also turns out you can define the fields of a class without initializing them, for instance:

public class Person {
    // will use default values
    String name;
    int age;
    String[] previousPhoneNumbers;

    // will use default values in the array
    int[] nums = new int[3];

    // will use the actual values you chose
    double income = 1000.22;
    String employer = "GWU";
    int[] randoms = {3, 2};
}  

Default values are either 0, false, or null, depending on the type – we saw this when creating arrays and only specifying their sizes. null is a value for any complex/reference type that represents an invalid/non-existant memory address.

Class Participation Activity 1 [5 min]

Imagine we have the following code (in another file for example) that creates a Person object using the default constructor (which we never wrote):

Person jane = new Person();

Let’s trace through, in memory, how this object would be set up in the heap, paying attention to the default values.

You can also use default values in regular constructors; it depends whether or not you want to specify any values when you specify the fields.

Note: If you start to write your own constructor(s) as above, you will lose the default constructor – Java’s expectation is you know what you’re doing and need something more specific than the default behavior.

The this keyword

Java allows any method that is associated with an object to retrieve its memory address using the this keyword, if you are inside the class:

public class FakeString {
    char[] letters = new char[255];
    int actualLength = 0;

    public boolean isLongerThanFive(){
        if (this.actualLength > 5)
            return true;
        return false;
    }

    ...
}  

The code above would work just the same without the this keyword, but it’s useful to disambiguate between arguments and fields:

public class Person {
    String name;
    int age;
    double salary

    public Person(String name, int age, double salaryIn){
        this.name = name;
        age = age; // this compiles but is a conceptual error
        salary = salaryIn;
    }
}  

In the example above, this.name = name; successfully updats the name field of the class with the incoming name arguemnt. However, age = age; only updates the local argument age; this local variable shadows the field because it has the same name. One would probably want this.age = age; instead, or to give the two variables unique names like salary vs salaryIn.

Anytime a method is called on an object in Java, you should write this as another local variable to that method on the stack.

The static keyword

Another keyword that can now make sense is static, which can be applied to fields to indicate that all objects of that class share that same field. For example:

public class GW_Student {
    String name;
    int GWID;
    static String school = "Colonials"; // GW's old nickname, see https://en.wikipedia.org/wiki/George_Washington_Revolutionaries

    public GW_Student(String name, int GWID){
        this.name = name;
        this.GWID = GWID;
    }
    
    public void updateSchool(String newSchool){
        school = newSchool;
    }
    
    public String toString(){
        return name + " " + GWID + " " + school;
    }
}  

public class Driver {
    public void runMe(){
        GW_Student ravi = new GW_Student("Ravi", "G123876");
        GW_Student jane = new GW_Student("Jane", "G666666");
        GW_Student jorge = new GW_Student("Jorge", "G123876");
        System.out.println(ravi.toString()); 
        System.out.println(jane.toString());
        System.out.println(jorge.toString());
    }
}

Now, imagine we wanted to update this software system to change Colonials to the new name, Revolutionaries. We could, in theory, update all of these objects individually with the updateSchool method:

ravi.updateSchool("Revolutionaries");
jane.updateSchool("Revolutionaries"); // unnecessary
jorge.updateSchool("Revolutionaries"); // unnecessary

But this is annoying to type, and even if we loop it we’re still wasting a String on all the objects that are just storing the same value. Instead, because school is static, we can just call updateSchool on any single object, and the change will be visible across all of them, since they all share this single field.

static methods

static can also be applied to a method to indicate you can call the method without having any objects created yet. These work together, in that static methods can only access static fields (and their own local variables) and not regular fields, and static methods can only call other static methods in the same class.

This was why most of the methods you saw this semester were static, because we were calling them from main, which is also static. Why is main static? Recall that when you type in the terminal java HelloWord there was no constructor/object ever created in that example.

Visibility

The last piece of syntax that we have yet to cover this semester is visibility, which refers to the public modifiers you’ve been seeing. There are a few other visibility flavors that we won’t cover this semester.

For now, when you see public, that means that code outside of that class can use the method and/or see the field. For example, if updateSchool wasn’t public, we could not have called it in the Driver class.

Visibility is orthogonal to the static modifier.

Next class

We’ll trace through code that uses constructors to create objects