Printing our ArrayIntList

Say we have an ArrayIntList object that we want to view the contents of. This could be valuable in a number of situations, for example when presenting data as part of our program or even when attempting to debug a program that uses ArrayIntList objects. We might try to write the following code:

ArrayIntList list = new ArrayIntList();
list.add(4);
list.add(7);
list.add(-3);
System.out.println(list);

With what we know about Java, what would we expect to be printed? Hopefully we would see a nice, readable representation of the state of our ArrayIntList object, such as

[4, 7, -3]

However, upon running the code above, we find that what is actually printed the console is

ArrayIntList@1f89ab83

“What is this garbage?”, you might be wondering!

When we attempt to print our ArrayIntList object, Java calls its toString() method to get a String representation of the object to print to the console. But we haven’t implemented a toString() method for our ArrayIntList class, so Java calls the default toString() method for objects instead, which returns the seemingly random output we see above (this is similar to what happened when we tried to print out Array objects without explicitly calling Arrays.toString).

As we mentioned before, we’d like to be able to print out a representation of our ArrayIntList object that is a bit more readable, and in order to do so we must override Java’s default toString() method for objects in our ArrayIntList class.

Implementing toString()

To match Java’s behavior for ArrayList<E>, we want ArrayIntList’s toString() to return the string we first expected to be printed above; that is, a comma-separated sequence of the values in our ArrayIntList that is surrounded by square brackets. For the following example code

ArrayIntList list = new ArrayIntList();
list.add(4);
list.add(7);
list.add(-3);
System.out.println(list);

we would want the following output

[4, 7, -3]

To begin, let’s write a method stub for our toString() method that will override Java’s default

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

    // constructors and other methods

    // post: returns a String representation of the current ArrayIntList
    // with comma-separated values surrounded by square brackets
    public String toString() {
        // TODO implement this method
    }
}

It’s important for us to note that this is a fencepost problem, with a comma and a space in between but not on either end of the values in our ArrayIntList (if you want to review what a fencepost problem is, see this slide deck from CSE 142). Keeping this in mind we might try to write the following method

public String toString() {
    String result = "[" + elementData[0];
    for (int i = 1; i < elementData.length; i++) {
    	result += ", " + elementData[i];
    }
    result += "]";
    return result;
}

An important part of programming and something that will be absolutely necessary for success in this course is the ability to test our code. Once we have a method we think will work, such as the above example, we should always test it. So let’s do so using the example at the top of the section, keeping in mind we want the following output

[4, 7, -3]

Upon running the code with our current toString() we get the following output:

[4, 7, -3, 0, 0, 0, 0, 0... continuing on for ~90 more values] 

We’ve printed out way too many values! Remember that we only want to show the client their view of the ArrayIntList, not our view as implementers. And what controls the client’s view? Our private field size tells us the number of elements from elementData that are a part of our ArrayIntList, and consequently, the number of elements we want to print out for the client. So rather than looping until elementData.length we use the following updated toString() which only loops to size.

public String toString() {
    String result = "[" + elementData[0];
    for (int i = 1; i < size; i++) {
    	result += ", " + elementData[i];
    }
    result += "]";
    return result;
}

Running the same test case, we find that we get the desired output

[4, 7, -3]

But does our method work for all cases? When testing code, it’s important to consider edge cases to make sure our program works for every case. For example, let’s consider the case where we have an empty ArrayIntList

ArrayIntList list = new ArrayIntList();
System.out.println(list);

We would want our code to produce

[]

but upon running our code, we find that it produces

[0]

Why is this? Well, we can see that in our current toString() method we unconditionally include the first value in our elementData array, even if the client hasn’t added any elements! To solve this, we must add a case for when size == 0, producing the following finished method

public String toString() {
    if (size == 0) {
        return "[]";
    } else {
        String result = "[" + elementData[0];
        for (int i = 1; i < size; i++) {
            result += ", " + elementData[i];
        }
        result += "]";
        return result;
    }
} 

We would want to test this code in multiple other cases (see below for examples), but we will forgo a thorough walkthrough of the testing process for the sake of time. The important takeaways from the toString() example are:

Implementing clear()

Another functionality that would be nice for clients to have is the ability to completely clear their ArrayIntList, rather than having to create a new ArrayIntList object every time they need a blank slate. For example, Java’s ArrayList<E> has a clear() method with the same functionality. We will implement the clear() method for ArrayIntList, starting with the method stub

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

    // constructors and other methods

    // post: removes all values from the current ArrayIntList
    // turning it into an empty ArrayIntList
    public void clear() {
        // TODO implement this method
    }
}

We might start with the following effort to implement the method, remembering that an ArrayIntList is initially filled with all values as 0

public void clear() {
    for (int i = 0; i < size; i++) {
    	elementData[i] = 0;
    }
    size = 0;
}

This works just fine, but do we need to do all of that looping? Remember, size is what controls the client’s view of ArrayIntList. As implementers, we never rely on any values in elementData outside of the range of 0 <= index < size since the client won’t view or interact with them. For example, anything that is beyond that range of values will get overwritten if the client adds more things to the list (this might become more clear if we remember how the appending add unconditionally overwrites the next hidden value elementData[size]). This is a great example of how we as implementers can use our size and elementData fields in order to produce the client view of a flexible ArrayIntList. We can now write a much cleaner, more readable, and more efficient clear() method

public void clear() {
    size = 0;
}

Does this really work? All that we need to remember to ease any concerns is that size ultimately determines what the client sees in our ArrayIntList (remember, we looped until size in our toString() method above!!). Recalling the radio example from lecture, we can view the values in elementData outside the range of 0 <= index < size as the wires inside of the radio (we as implementers have access to them, but the client doesn’t see or interact with them). As long as size == 0 the ArrayIntList will appear to be empty to the client, regardless of the other values in elementData we may have access to as implementers.

Practice

Try out this problem and this other problem for more practice with ArrayIntList