Link Menu Search Expand Document

More ArrayIntList; pre/post conditions; exceptions

ed Lesson

Table of contents


Review

We have learned a lot this week about object-oriented programming! Some of the highlights include:

  • How to define our own classes, and how to manage state (fields) and define behaviors (methods) for those classes.
  • How to create instances of the classes and work with objects.
  • How to create abstractions that separate the client from knowing how our class works internally.
  • How to enforce access to a class’s internal fields by encapsulating state with the private modifier.

These are all concepts that can be tricky to wrap your head around the first time you see them since they require a different way to think about the programs than what you are used to. It can also be hard to see why we care about abstractions and encapsulation until we work on larger programs.

One motivation is that, unlike other engineering disciplines, software is effectively unconstrained by the laws of physics. Chemical engineers, for example, have to worry about temperature. Civil engineers have to worry about the properties of materials. For software engineers, the greatest limitation we face is understanding exactly what we’re building.

Other instances

What if we wanted to write methods for ArrayIntList that took other ArrayIntList instances as parameters? This sounds kind of weird at first but can be really helpful! For example, the ArrayList class has a method addAll that takes another ArrayList and adds all the values in that list to this one.

Let’s implement the addAll method for ArrayIntList.

Client

ArrayIntList list1 = new ArrayIntList();
list1.add(1);
list1.add(2);
list1.add(3);

ArrayIntList list2 = new ArrayIntList();
list2.add(4);
list2.add(5);

list1.addAll(list2);

System.out.println(list1);
// [1, 2, 3, 4, 5]

We can start by writing a method with the correct header

Implementer

public class ArrayIntList {
    private int[] elementData;
    private int size;

    ...

    // post: adds all the values from other to the end of this list
    // the values will appear at the end in the same order they appear in other
    public void addAll(ArrayIntList other) {
        // TODO
    }
}

In the addAll method, we’re now working with two different instances of the ArrayIntList class.

  1. The secret variable this refers to list1.
  2. The parameter other refers to list2.

We want to add each element of list2 to list1. To make this happen, in the addAll method, our code needs to call this.add on each element of other.

Implementer

public void addAll(ArrayIntList other) {
    for (int i = 0; i < other.size(); i++) {
        this.add(other.get(i));
    }
}

Private access

Here is the same method implemented by accessing the fields of other directly.

public void addAll(ArrayIntList other) {
    for (int i = 0; i < other.size; i++) {
        this.add(other.elementData[i]);
    }
}
Will this code compile?

Yes! Even though the elementData and size fields are declared private, Java’s definition of private has some technicalities to it.

In Java, private doesn’t mean private to things outside this instance, it means private to things outside of this class. While that looks like a tiny wording difference, what it implies is that any instance of ArrayIntList has the ability to access fields of any other ArrayIntList.

Both this example of addAll and the earlier example are good style. Neither approach is significantly faster than the other. But when we implement linked lists in a couple weeks, we’ll see how some of these assumptions break down due to The Law of Leaky Abstractions.

Pre/post conditions

When we implemented get and set, there were some situations that our ArrayIntList couldn’t handle.

Give an example of a problematic input to get.

A simple example would be an ArrayIndexOutOfBoundsException by calling get(-8) or get(100000). A more subtle example is any index greater than or equal to the current list size, since get would return the “garbage” value in a vacant space.

Preconditions
Conditions that the client needs to follow in order for the method to behave as intended.
Postconditions
The intended behavior of a method assuming preconditions hold.

Implementer

// pre : 0 <= index < size()
// post: returns the element at the given index
public int get(int index) {
    return elementData[index];
}

While these comments are helpful for the client, what’s even better is to add checks in the code to mitigate or prevent unintended behavior from occurring when the preconditions are not met.

  1. We can return a specific dummy value or print a message to the console in order to notify the client. For example, some implementations of indexOf will return -1 if the element is not in the array. Since the client knows that the only valid values are in the range 0 <= index < size(), -1 signifies a false result. This behavior should be documented!
  2. We can stop the program entirely if there’s no acceptable way of handling the problem. We’ll learn how to do this using Java exceptions.
Why is it a bad idea for get to return -1 when given an invalid index?

The client might have actually added the value -1 to the list earlier, so they wouldn’t be able to know if the -1 returned from get represented the dummy value or the value that they added to the list.

Exceptions

We can check the preconditions by adding an if statement. In order to stop the program within the if statement, we use the Java throw syntax.

Implementer

// pre : 0 <= index < size() (throws IllegalArgumentException if not)
// post: returns the element at the given index
public int get(int index) {
    if (...) {
        throw new IllegalArgumentException();
    }
    return elementData[index];
}

In this example, we chose to use a new IllegalArgumentException. There are hundreds of different types of exceptions, each of which informs the client of a different kind of problem. We don’t need to worry too much about the particulars of which type of exception to use.

Notice that we also documented this exception by adding it to our method comment.

What condition should we use within the parentheses of the if statement?

We want to enter the if branch when the preconditions are not met, or when the index < 0 or index >= size().

Class constants

Programs are meant to be read by humans, not by computers. We want our code to say what we mean as clearly as possible. Let’s look back at our version of ArrayIntList.

Implementer

public class ArrayIntList {
    private int[] elementData;
    private int size;

    public ArrayIntList() {
        elementData = new int[10];
        size = 0;
    }
}

If someone else were to read our code, they might ask, “Why is the number 10 there? Is there any reason it’s not 9?”

In this program, the number 10 is what’s called a magic value. As the implementers, we wanted 10 to be the default capacity of elementData. However, to someone new to this program, it’s not necessarily immediately clear that the number 10 means “default capacity”. (They might assume that the number 10 might have important meaning elsewhere in the class, so they might end up having unnecessarily to read the rest of the class to answer this qusetion.)

One solution is to just write a comment indicating the meaning of the value 10. That’s not a bad idea. However, if we use the number 10 in many in places in the class, it would be better to have the flexibility of changing it without having to change every single place that it appears. A better solution is to store magic values in a class constant with a descriptive name that we use in our code instead of that magic value.

Implementer

public class ArrayIntList {
    public static final int DEFAULT_CAPACITY = 10;

    private int[] elementData;
    private int size;

    public ArrayIntList() {
        elementData = new int[DEFAULT_CAPACITY];
        size = 0;
    }
}
  • Class constants are usually declared public static final. There may be some cases where we want those to be different, but every class constant in CSE 143 should have all of these modifiers.
  • By convention, class constants follow an UPPER_CASE naming format (affectionately known as YELLING_CASE).
  • Everywhere we need the default size we will now instead use the constant DEFAULT_CAPACITY.

Class constants are even more helpful when they replace magic values in multiple places. Now, if we want to change the default capacity, we only need to change it once at the top of the class instead of having to manually modify every instance. This way, we avoid forgetting to change some of them and also avoid bugs that arise from accidentally changing other magic values that happen to be set to 10.

Multiple constructors

As implementers, we can provide different ways for clients to construct a new instance of a class. For example, what if we would like the client to be able to construct a new ArrayIntList with a particular capacity? We would want to define a new constructor with an int capacity parameter.

Implementer

public class ArrayIntList {
    public static final int DEFAULT_CAPACITY = 10;

    private int[] elementData;
    private int size;

    public ArrayIntList() {
        elementData = new int[DEFAULT_CAPACITY];
        size = 0;
    }

    public ArrayIntList(int capacity) {
        elementData = new int[capacity];
        size = 0;
    }
}

Notice that there’s a bit of redundancy in this code. The two lines in each of the constructors are nearly identical. Use this(...) to call another constructor from within a constructor.

Implementer

public class ArrayIntList {
    public static final int DEFAULT_CAPACITY = 10;

    private int[] elementData;
    private int size;

    public ArrayIntList() {
        this(DEFAULT_CAPACITY);
    }

    public ArrayIntList(int capacity) {
        elementData = new int[capacity];
        size = 0;
    }
}
What would happen if we replaced this(...) with new ArrayIntList(...) instead?

Whenever we use the new keyword, a new object is created. We can even create new objects as we are creating an object. Calling new ArrayIntList(DEFAULT_CAPACITY) creates a second ArrayIntList inside of the constructor!

It might help to visualize the following code that assigns the result to a variable.

Implementer

public ArrayIntList() {
    // this(DEFAULT_CAPACITY);
    ArrayIntList list = new ArrayIntList(DEFAULT_CAPACITY);
}