Implementing ArrayIntList
Table of contents
- Classes and objects
- Implementing objects
- this instance
- Printing an ArrayIntList
- Clearing an ArrayIntList
- Style
- Private fields
Classes and objects
In most of CSE 142, we developed Java programs inside of a large class that represented a fully-functional program. In CSE 143, we’ll shift towards a different use for classes: as a template for a type of objects. The ArrayList
class provides a template for list objects.
Objects combine data and behavior. For example, the final example of our list contained the data, “According to all known laws of aviation” and the behavior of being able to add
or remove
elements from the list. We saw how clients and implementers can view the same list in two different ways.
This idea of separation between the client and the implementer is known as abstraction.
Abstraction
Think about a radio. Have you used a radio before, such as one in a car? Many of us have interacted with radios or other technology such as computers. But have you built a radio from scratch before? This is much more complicated.
This analogy is similar to the distinction between clients and implementers. As clients of a radio, we know how to use a radio but not necessarily how to build one. This is part of what makes a radio actually useful. If everyone who ever wanted to use a radio needed to learn exactly how a radio functions just to use one, radios would never have been broadly accessible to a large audience.
In this course, we’ll explore both the client and implementer perspectives of Java collections.
Describe the client's view of the add method.
The add
method takes an element and appends it to the end of the list.
Describe the implementer's view of the add method.
Given an element, the add
method takes the element and stores it in the next vacant space in the 10-element data array. The size
variable of the list increases by 1.
Implementing objects
In this lesson, we’ll implement an ArrayIntList
from scratch. We might start with the following code.
Implementer
public class ArrayIntList {
// Constructs an empty list
public ArrayIntList() {
elementData = new int[10];
size = 0;
}
}
What are the problems with this class?
Neither elementData
nor size
have declared variable types, so this code won’t compile.
One way to fix the code so that it compiles is to add the variable types to the lefthand side of each assignment statement.
Implementer
public class ArrayIntList {
// Constructs an empty list
public ArrayIntList() {
int[] elementData = new int[10];
int size = 0;
}
}
This brings up a different issue. If we want to implement an add
method, then it won’t be able to access the elementData
or size
. (In fact, both of these variables disappear the moment the program leaves the constructor because the variables are declared within the constructor.)
Fields
To address this, we declare fields inside the class but outside of any particular method. Fields are variables that are associated with an individual object. This way, any method in the ArrayIntList
class can access the elementData
or size
variables.
Implementer
public class ArrayIntList {
int[] elementData = new int[10];
int size = 0;
// Constructs an empty list
public ArrayIntList() { }
}
However, there’s a style issue with this code. Say we want to allow the user to specify the length of elementData
. Assigning int[] elementData = new int[10]
results in every ArrayIntList
object starting with an elementData
with capacity for 10 elements.
Instead, we prefer to declare fields in the class but initialize them in the constructors.
Implementer
public class ArrayIntList {
int[] elementData;
int size;
// Constructs an empty list
public ArrayIntList() {
elementData = new int[10];
size = 0;
}
// Constructs an empty list with the given capacity
public ArrayIntList(int capacity) {
elementData = new int[capacity];
size = 0;
}
}
External brain example
As an example of how this section might contribute to your external brain, here’s a list of steps for writing a new Java class. As you complete the rest of this lesson, update this example with new rules and insights that you discover.
- Create a new Java file named after the class. Inside the file, write,
public class [class name] { ... }
. - Declare fields at the top of the class. (What are fields, and how do they relate to classes and objects?)
- Define one or more constructors that assign values to each field. Write,
public [class name]([constructor args]) { ... }
for each constructor.
this instance
When there are multiple instances of a class like list1
and list2
, Java determines which one you’re referring to with the help of a secret variable called this
. The following implementation of the add
method actually uses this
even though the code doesn’t say so.
Implementer
public class ArrayIntList {
int[] elementData;
int size;
// appends the given value to the end of the list
public void add(int value) {
elementData[size] = value;
size++;
}
}
Java knows to reference the current instance’s elementData
and size
by automatically adding the special variable this
before parameters and variables that aren’t declared in the method. The above implementation can be rewritten to include this
to make reveal what Java is doing behind the scenes.
Implementer
public class ArrayIntList {
int[] elementData;
int size;
// appends the given value to the end of the list
public void add(int value) {
this.elementData[this.size] = value;
this.size++;
}
}
When list1.add(1)
is evaluated, the add
method is called with this
as list1
. When list2.add(2)
is evaluated, the add
method is called with this
as list2
.
These two programs are functionally equivalent. We don’t really consider one version to be better style than the other. What’s important is to pick a rule and stick with it.
Printing an ArrayIntList
In order to view the contents of our ArrayIntList
, we had to print out all of the elements individually.
Client
ArrayIntList list = new ArrayIntList();
list.add(1);
list.add(97);
System.out.println(
"["
+ list.elementData[0] + ", "
+ list.elementData[1]
+ "]"
);
While printing each element in elementData
works, this is inconvenient for clients (users) of the ArrayIntList
class since they need to know the implementation details of ArrayIntList
. This is also unsafe since clients might accidentally print out elements that are not contained in the list. Using the same code to print an empty list will lead to unexpected results.
What is the correct expected output for printing an empty list?
Just []
since there are no elements in the list. The underlying elementData
array can still contain 10 elements, but the size
variable is 0! This indicates that the list does not contain any elements.
Client
ArrayIntList list = new ArrayIntList();
System.out.println(
"["
+ list.elementData[0] + ", "
+ list.elementData[1]
+ "]"
);
This prints out [0, 0]
since the default value for each int
element in the elementData
is 0. Unfortunately, printing out the list
variable won’t work.
Client
ArrayIntList list = new ArrayIntList();
System.out.println(list);
What actually prints to the console is ArrayIntList@1f89ab83
. Thanks, Java!
How println works
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. The default method is responsible for returning the seemingly random output above.
Next, we’ll learn how to override Java’s default toString()
method so that printing an ArrayIntList
results in a more helpful output.
Clearing an ArrayIntList
It would be nice for clients to be able to completely clear their ArrayIntList
rather than having to create a new ArrayIntList
object every time they need an empty list. Java’s ArrayList<E>
has a clear()
method with this exact behavior.
One way to implement clear()
is to remember that the elementData
in an ArrayIntList
object is initially filled with the default value of 0
.
Implementer
public void clear() {
for (int i = 0; i < size; i++) {
elementData[i] = 0;
}
size = 0;
}
This works just fine, but do we really need 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 can’t view or interact with them. For example, anything that is beyond that range of values will be overwritten by the add
method. We can now implement clear()
more concisely and more efficiently with just a single line of code.
Implementer
public void clear() {
size = 0;
}
Explain why this works.
Remember the loop condition for toString()
? The size
variable entirely determines the useful spaces in the array. Recalling the radio analogy from before, the values in elementData
outside the range of 0 <= index < size
are like the wires inside of a radio. As implementers, only we have access to them. As long as size == 0
, the ArrayIntList
will appear to be empty to the client, regardless of the values in elementData
.
Style
Programming languages communicate algorithms to other people, not the computer. We study computer programming in Java not because it’s the fastest language (it’s not), not because it’s the most well-designed language (it’s not), but primarily because Java is a programming language understood by many other people.
The readability and understandability of code is an important property of a program. Style is a way to describe what makes code easy (or hard) to understand. But every software development organization has its own unique style guidelines. The important skill for us to learn is how to adapt our code to meet particular style guidelines.
Documenting code
Every method needs to have a descriptive comment about its behavior without including implementation details. It would not be useful for the user of a radio to read the manual only for it to say:
If you turn this knob, it changes the electromagnetic signal in this way, which then causes this change…
The goal of documenting code is to describe what happens without going into detail about how. We document code by adding comments above the method header. Here’s a first try at documenting the add
method.
Implementer
// Adds a value at index size in elementData and increments size
public void add(int value) {
elementData[size] = value;
size++;
}
Why is this comment problematic?
The client of an ArrayIntList
doesn’t want to know about the implementation details of the list, so mentioning the size
and elementData
fields increases their burden. In order to know what this method does, they now also need to understand what those fields mean when it would be simpler to describe the behavior in terms of their mental model of a list.
The implementer of ArrayIntList
also doesn’t necessarily benefit from this comment either. The comment says exactly what the code says, except with more ambiguity. The implementer can get the same message from reading the two lines of code. Including the comment might even confuse them if they have a different interpretation for how the method “adds a value”.
Here’s another try at documenting the add
method.
Implementer
// Adds a value to the list
public void add(int value) {
elementData[size] = value;
size++;
}
This comment addresses the problem of going into too much detail. But we might want to improve it by specifying that the value is added to the end of the list: “Adds a value to the end of the list.”
Later, we’ll study a bad ArrayIntList
implementation and identify all of the style issues.
Private fields
There’s a big problem with our ArrayIntList
class right now. Since fields are just variables, the client can modify the fields!
Client
ArrayIntList list = new ArrayIntList();
list.size = -8;
Every engineering artifact has certain conditions of operation. For example, personal devices such as laptops and smartphones come with warranties. These warranties are broken when unauthorized changes are made to the device.
Give an example of a call that would cause an error after setting size to -8.
Any future call to add
would fail since the code would attempt (and fail) to reassign elementData[-8]
. If the size
was set to a larger value than expected, we might also run into problems with toString
printing out placeholder values for the vacant spots in elementData
(or cause an error).
In programming, this idea of preventing unauthorized changes to the fields of an object is known as encapsulation. Declare fields private
so that they can only be accessed and modified by the implementing class.
Implementer
public class ArrayIntList {
private int[] elementData;
private int size;
...
}
The client code from the first example will no longer compile since it cannot modify the size
field. But it also prevents the client from accessing the size
as well, so they can no longer know the size of the list. There’s no special Java keyword for making fields read-only.
Getters and setters
Instead, Java programmers often define simple methods known as getters and setters to control field access and modification. We can implement a getter method for the size
field by defining a new method getSize()
or simply size()
.
Implementer
public class ArrayIntList {
private int[] elementData;
private int size;
public int size() {
return size;
}
...
}
This allows the client to get the size of the list by calling the size()
method rather than accessing the private size
field.
Client
ArrayIntList list = new ArrayIntList();
int currentSize = list.size();
currentSize += 1;
Why does reassigning currentSize not change the size field?
Calling list.size()
returns a local copy of the list’s current size. Any modifications to this local copy won’t affect the encapsulated list’s size
field.
An important getter method for lists is the get
method that returns the element at a given index.
Implementer
public class ArrayIntList {
private int[] elementData;
private int size;
public int get(int index) {
return elementData[index];
}
...
}