Chapter 8

Arrays

Storing More Data in Lists & Arrays!

Subsections of Arrays

Introduction

So far, we’ve learned how to store many different types of data in variables. We can then use those variables in our programs to receive user input, store the results of calculations and so much more.

However, we have to individually declare and maintain each variable in our code. As we build larger programs, we’ll have to deal with a commensurately large amount of variables, and it can be hard to keep track of all of them.

At the same time, we may also want to store many related items together. For example, if we are building a program to calculate student grades, we may want to be able to store each grade a student has received. With what we’ve learned so far, we might have to create variables named assignment1, assignment2, assignment3, and so on, to store this information. This can quickly become very difficult to manage, and it is difficult to write code that can perform calculations on those variables. Each time a new variable is added, every location in the code that references those variables would need to be changed.

Is there a better way to do it? YES!

Nearly every programming language supports special types which aggregate, or collect data in a single variable. In languages which support objects, these types are instance classes which may have methods and attributes.

Chapter References

Theory

YouTube Video

Video Materials

In Theory

Arrays are a special type of variable that can store multiple items of the same type, so ‘int’ or ‘bool(ean)’. Some programming languages, such as Java, comes with support for arrays built into the language. While other languages, such as Python, require us to adapt other aggregate data types to build arrays.

In general, we will stick with storing items of the same type in each array.

A great way to think of an array is like a set of post office boxes that we might see at our local post office, as shown in the image below. Each box has a number that uniquely identifies it. So, if we know which number is ours, we can easily receive our mail.

Post Office Boxes Post Office Boxes1

Similarly, the post office can look at the address line on the letter to find the post office box number it should go to, and then they can place it in the correct place.

An array works in a very similar fashion. Arrays consist of a set of boxes with sequential numbers, starting at 0. So, an array that has 5 boxes, or elements, would have 5 boxes, numbered 0 through 4. The number for a particular element is called its index.

For example, let’s consider an array that contains 5 elements, and each element is one of the first five letters of the alphabet as shown in the image below.

Array Indexes and Elements Array Indexes and Elements

In this array, we’d say that the element at index 0 is a.

Multidimensional Arrays

One of the most powerful features of arrays is the ability to create a multidimensional array. A multidimensional array is an array that contains another array within each element, sometimes many layers deep. This is very useful when we need to store data in a grid layout, for example.

The image below shows how data might be represented in a 2-dimensional array. The element x is located in row 1, column 3. So, it would be part of the array stored in index 1 in the primary array, and the element itself is at index 3 in the array stored there.

2-Dimensional Array 2-Dimensional Array

We’ll learn how to work with arrays in a specific programming language later in this chapter.


  1. File:USPS Post office boxes 1.jpg. (2017, May 17). Wikimedia Commons, the free media repository. Retrieved 18:17, November 5, 2018 from https://commons.wikimedia.org/w/index.php?title=File:USPS_Post_office_boxes_1.jpg&oldid=244476438↩︎

Subsections of Theory

Iteration

YouTube Video

Video Materials

Of course, once we’ve created an array of data, we need some way to access its elements easily in our code. Thankfully, we can use the loop structures we’ve learned in a previous chapter to iterate across our arrays. Iteration is the term we use to describe accessing each element in the array and possibly performing some repeated action using that element.

For example, consider the flowchart below, showing a simple program that uses loops and arrays:

Array Iteration Flowchart Array Iteration Flowchart

This program begins by accepting a single input from the user, stored in the variable x. That input is used to determine the size of an array, denoted by the array(x) block. Each programming language has its own way of creating an array, but we’ll use this simplified form in these flowcharts.

Next, the program reaches a For loop which uses i as its iterator variable. That variable will include all values from 0 up to, but not including, the value stored in x. Notice that there is a parenthesis to the right of x, showing that it is not included in the sequence denoted by [0, x).

Inside of that loop, we receive another input from the user, stored in the variable y. Then, we store that value into the array, using our iterator variable i as the index in the array. So, the first input we receive and store in y will be stored in a[0], the first element of the array. In most programming languages, we use square brackets [] after an array variable to denote a specific element in the array, with the index of that element shown inside of the square brackets. When the loop repeats, the next value will be stored in a[1], and so on, until the array contains x values.

Once the first For loop terminates, we create a new variable named sum and set it initially equal to 0. We’ll use this variable to add up all of the numbers in the array in the next For loop. However, that loop is defined a bit differently. In the flowchart we see that the loop is defined as j : a, which means that we are using j as our iterator variable, but instead of getting values from a mathematical sequence such as [0, x) from the first loop, we are taking the items directly from our array, a, instead. These special For loops are sometimes known as For Each loops or Enhanced For loops, depending on the language. In essence, they repeat the loop one time for each element in the array given. So, we’d say “for each j stored in a, add j to sum” to describe this loop.

The first time we run the code inside of that loop, the variable j will be storing the value of the some element in array a. So, we could say that j has the same value as a[0]. We’ll add that value to the sum variable, then repeat the loop. In the next iteration, the variable j will then store a different value in a, or a[1]. We’ll continue to repeat the loop until each element in a has been used–the loop ensures we have seen all the elements exactly once.

At the end, when we output the sum variable, it should be the sum of all of the elements in the array.

Of course, we can easily rewrite these loops as While loops instead, or we could use the standard form of the For loop as the second loop, using the iterator variable to refer to the index of the element inside of the array instead of the element itself.

While, For, and For Each

As a general rule:

  1. Use a While loop when the decision to “loop again” depends on the calculations and logic of the loop body.
  2. Use a For loop when the number of loops you are going to make is fixed.
  3. Use a For-Each loop when all the following are true:
    1. the language supports it;
    2. you do not care what order the data is looped through;
    3. you want to ensure every element of the aggregated variable is examined;
    4. you do not change any data in the aggregate variable (look but don’t touch).

Consider taking one egg at a time out of full egg carton.

  1. If you were going to crack eggs until you had at least one cup of egg-goop, that is a while loop.
  2. If you are going take 3-eggs for an omelet (a fixed number of eggs), that is a for loop.
  3. If you are going to swap the eggs around in the carton, or maybe draw faces on them, that is also a for loop (there are a dozen eggs). Here you are changing the “elements” in the carton.
  4. If you are going to weigh each egg, then put it back where it came from–that is a for-each loop,

Subsections of Iteration

Call by Reference vs. Value

YouTube Video

Video Materials

As we start creating objects, one major concept is how objects are handled differently than “primitive” data types when used as arguments; i.e. when passed to functions. Typically, there are two scenarios: call by value and call by reference. Let’s discuss them both in detail to understand how they differ and the impact that may have on our code.

Call by Value

When method parameters are handled in a call by value way, that means that each time we call the method, that method gets a unique copy of the parameter. So, any changes made to that parameter won’t be reflected in the code from which the method was called.

Let’s look at an example:

public static void main(String[] args){
  int x = 5;
  int y = addThree(x);
  System.out.println(x);
  System.out.println(y);
}

public static int addThree(int x){
  x = x + 3;
  return x;
}

In this code, we initialize the variable x to the value $ 5 $. Then, we pass that value as an argument to the method addThree, where it is stored in the parameter named x. Inside of that method, we add $ 3 $ to that variable, then return its new value.

Back in the main method, when we print the value of x, it will still be $ 5 $. Why? Since this method is using call by value, the method addThree gets a copy of the value stored in x, which it stores in its own variable. So, when the method updates the value in x it is only changing the value in it’s copy. The original value from the main method is unchanged.

However, the value y in the main method stores the value returned by addThree, which is $ 8 $. So, when we print that value, we’ll get $ 8 $ as expected.

Call by Reference

If a method handles parameters in a call by reference way, the method simply gets a reference, sometimes called a pointer, to the value passed as an argument. In that way, when it changes the value of that argument, it is also changing the value of the original variable passed in as that argument.

Here’s another example:

public static void main(String[] args){
  int[] x = {1, 2};
  changeLast(x);
  System.out.println(x[0]);
  System.out.println(x[1]);
}

public static void changeLast(int[] x){
  x[1] = 5;
}

In this example, we are creating an array named x in the main method, which stores two values, $ 1 $ and $ 2 $. Then, we use the changeLast method to change the last element in the array to $ 5 $. Notice that we are providing the array x as an argument to the changeLast method, but that method does not return a value.

When we print the first element of x, it is $ 1 $ as expected. However, when we print the second element of x, we see that it is now $ 5 $, even in the main method. Why? This is because the method changeLast is using call by reference, so instead of getting a copy of the variable x, it actually gets a reference to where that array is stored in the main method. Therefore, any changes made to x in changeLast also affect the value in main

Reassignment

However, it is very important to know that we cannot reassign the value of a variable passed using call by reference and expect it to work the way we want. Here’s an example:

public static void main(String[] args){
  int[] x = {1, 2};
  changeLast(x);
  System.out.println(x[0]);
  System.out.println(x[1]);
}

public static void changeLast(int[] x){
  x = {3, 4};
}

In this instance, we are reassigning the variable x inside of changeLast to an entirely new array. By doing so, we are changing the reference that it uses to point to the new array. The old array still exists, and the variable x in main still refers to that array. So, when we print the values of x in main, we’ll get $ 1 $ and $ 2 $, since they haven’t changed.

In the example above that worked, we are simply changing a value inside of the array, not reassigning the array itself.

Different Scenarios

Unfortunately, it can be very difficult to tell on the surface which parameter handling method is being used by a particular piece of code, especially pseudocode. Each language handles this a bit differently, so we’ll have to carefully read our language’s documentation to know for sure. Later in this chapter we’ll discuss the specifics for the language we are using.

However, there are some general rules that we can follow that work in most cases:

  1. Simple data types, such as ints, floats, booleans, etc., are typically passed using call by value. Any changes to their value will not be seen outside of the method.
  2. Other data types, such as arrays, lists and objects are typically passed using call by reference. If the object can be modified, those changes will appear outside of the method. However, if the parameter is reassigned, it will not affect the outside reference

When in doubt, we can always write a simple test program in any language to check and see how parameters are handled.

Subsections of Call by Reference vs. Value

Chapter 5.J

Java Arrays

Arrays in Java

Subsections of Java Arrays

Array Creation

As with any variable in Java, we must declare an array variable before we can use it.

Array Declaration

To declare an array, we’ll need to provide the type, a variable name, and square brackets [] to denote that this variable is an array.

The preferred way of doing so is: <type>[] <variable_name>; as we can see in these examples:

//integer array
int[] a;

//boolean array
boolean[] b;

//double array
double[] d;

We can create an array of an available type in Java, including all primitive data types and object types. We can even create our own classes and store objects created from those classes in an array.

One easy way to remember this syntax is to say “I’m creating an int array named a (int[] a).” Try saying it with the examples above!

There is an alternative syntax as well:

<type> <variable_name>[];

though it is rarely used in practice. It makes less sense to say “I’m creating an int named x array,” doesn’t it?

Array Initialization

Once we have declared an array, we can initialize it just like any other variable. In Java, use the new keyword to create an array. We’ll also need to provide it with a size, which must be a positive, whole integer.

The standard format for this is:

<variable_name> = new <type>[<size>];

as we can see in these examples:

//integer array
int[] a;
a = new int[5];

//boolean array
boolean[] b;
b = new boolean[10];

//double array
double[] d;
d = new double[15];

Of course, we can combine both the declaration and initialization into a single statement:

//integer array
int[] a = new int[5];

//boolean array
boolean[] b = new boolean[10];

//double array
double[] d = new double[15];

If we know the values we’d like to store in the array when we initialize it, we can use the shortcut syntax to build the array directly:

<type>[] <variable_name> = {<item1>, <item2>, <item3>, ... , <itemN>}

as we can see in these examples:

//integer array
int[] a = {1, 2, 3, 4, 5};

//boolean array
boolean[] b = {true, false, true, false};

//double array
double[] d = {1.2, 2.3, 3.4};

Otherwise, we can store items individually in the array, as we’ll see in the next page.

new Keyword

The keyword new is used to create a object. Objects are a collection of data and the methods used to access and manipulate that data.

When you instantiate (the technical term for create) an object, a memory cubby hole, is created and given that name. You may then use various methods to assign, manipulate and change the data it holds. In general you must use new to get a distinct object in memory.

  1. In the figure below we first instantiate the int-array “a”.
  2. On line 2 we assign the int-array “b” the value of “a”. Note that we did not use new, so no new array was set up. Instead “b” simply points to “a”, “b” has become an alias for “a”.
  3. On line 3, since “a” and “b” are the same object, assigning “0” to b[0] is the same as assigning 0 to a[0].
  4. On line 4 we instantiate a new array for “b”.

Object creation flow chart Object creation flow chart

Why Don’t Arrays Use the New Keyword?

Arrays as data types pre-date object oriented languages by several decades, they are one of the oldest aggregate data types. So when Java appears on the scene in the 1990s, they adopted the “C-style” array definition syntax int[] a = {...} in addition to the more conventional int[] a = new int[]. Most object oriented languages will have some type of “syntactic sugar”, or short-hand syntax, for array and list creation.

Accessing Array Elements

Once we’ve created our array, we can access individual array elements by placing the index of that element inside of the square brackets [] following the variable name.

Array indexes in Java start at 0. So, to access the first element of an array named x, we would use x[0].

Similarly, the last array index is one less than the total size of the array. If the array named x has a size of 5, then the last element would be x[4].

Let’s take a look at an example in the code below. It will create an array of 5 integers, assign those integers a value, then sum them up and print the result.

public static void main(String[] args){
  //create an integer array
  int[] a = new int[5];

  //assign array elements
  a[0] = 5;
  a[1] = 10;
  a[2] = 15;
  a[3] = 20;
  a[4] = 25;

  //create a sum variable
  int sum = 0;

  //add up all the elements in the array
  sum = sum + a[0];
  sum = sum + a[1];
  sum = sum + a[2];
  sum = sum + a[3];
  sum = sum + a[4];

  //print the sum (it should be 75)
  System.out.println(sum);
}

Feel free to copy this code to a file named Array.java and modify the code to try other operations with arrays. Notice that the code above is simply a main method, so you’ll have to add the appropriate class declaration for the Array class in order to run this code!

Array Length

Arrays in Java have a fixed length, which is set when they are initialized.

To get the length of an existing array, use the built-in length property:

int[] a = new int[5];
boolean[] b = {true, false, true, false};

System.out.println(a.length); // 5
System.out.println(b.length); // 4

While it may seem obvious what the size of each array is in this example, there are many instances where we’ll be given an array with unknown size from another piece of code. Similarly, as we start working with loops and arrays, we’ll find that it is very useful to be able to access the length of the array directly.

Multidimensional Arrays

To create a multidimensional array in Java, simply provide additional square brackets [] as part of the array declaration. Similarly, when we initialize the array, we’ll need to provide a length for each dimension of the array:

//2d integer array
int[][] a = new int[5][5];

//3d boolean array
boolean[][][] b = new boolean[2][4][8];

//4d double array
double[][][][] c = new double[5][4][6][2];

We can also directly initialize the array elements, just as we did before. We’ll need to include additional curly braces {} for each dimension:

//2d integer array
int[][] a = {{1, 2}, {3, 4}, {5, 6}};

Typically, multidimensional arrays will only have 2 or 3 dimensions, but Java will allow up to 255 dimensions.

Each of the examples above will create a multidimensional array with each dimension consisting of arrays of the same length. However, it is possible to have arrays of different length within the same dimension, though it can be much more difficult to manage that scenario. We won’t deal with that in this course, but it may be useful in some situations.

To get the size of each dimension, we can use the length property on an element at that dimension:

//2d integer array
int[][] a = {{1, 2}, {3, 4}, {5, 6}};

System.out.println(a.length); // 3
System.out.println(a[0].length); // 2

Of course, we’ll need to make sure that a[0] exists before trying to find its length. If not, it will cause an error.

To access or assign elements in a multidimensional array, add additional square brackets [] for each dimension:

//2d integer array
int[][] a = {{1, 2}, {3, 4}, {5, 6}};

a[0][1] = 10;

System.out.println(a[0][1]); // 10
System.out.println(a[1][1]); // 4
System.out.println(a[2][0]); // 5

Again, remember that the indexes in each array dimension begin at 0. So, a[1][1] is actually accessing the second element in the first dimension of a, which is an array containing {3, 4}, and then the second element in that array, which is 4.

Array Operations

We’ve already covered one of the operations we can perform on an array, length. Let’s look at one more.

Copying Arrays

Java has a built-in method to copy sequential elements from one array to another:

System.arraycopy(source, sourcePosition, destination, destinationPosition, length)

Just like the method we use to print to the terminal, System.out.println, this is just another method that is available in System that we can use in our code.

That method accepts 5 inputs:

  • source - The array containing the elements to be copied
  • sourcePosition - An integer giving the starting position of elements to be copied from the source array
  • destination - An array to copy elements into
  • destinationPosition - An integer giving the starting position for elements to be copied to the destination array
  • length - The number of elements to be copied

The source and destination arrays must store compatible data types. Likewise, if we give a length which tries to copy too many elements from the source array, or if it goes past the end of the destination array, an error will occur.

Here is an example of how it can be used to copy elements from one array to another:

int[] a = {1, 2, 3, 4, 5, 6, 7};
int[] b = new int[3];

System.arraycopy(a, 1, b, 0, 3);

System.out.println(b[0]); // 2
System.out.println(b[1]); // 3
System.out.println(b[2]); // 4

To learn more about this method and how to use it, consult the official Java documentation linked below.

Reference: System.arraycopy

Array Loops

One of the most powerful ways to use arrays is to combine them with loops. Loops provide an easy way to perform operations on all elements in an array, no matter what the size of the array is.

Let’s go back to the flowchart seen earlier in this chapter, and see if we can implement that program in Java.

Array Iteration Flowchart Array Iteration Flowchart

First, we’ll need to start with a program that reads information from the keyboard. Let’s place this code in ArrayLoops.java, which should be open to the left:

// Load required classes
import java.util.Scanner;

public class ArrayLoops{
  
  public static void main(String[] args) throws Exception{
    
    // Scanner variable
    Scanner reader = new Scanner(System.in);
    
    /* -=-=-=-=- MORE CODE GOES HERE -=-=-=-=- */
    
  }
  
}

For the rest of this example, we’ll look at a smaller portion of the code. That code can be placed where the MORE CODE GOES HERE comment is in the skeleton above.

Creating the Array

Looking at the flowchart above, we must first accept one input from the user, an integer x that gives the size of the array. Then, we want to create an array named a which is able to store those elements.

int x = Integer.parseInt(reader.nextline());
int[] a = new int[x];

In the code, we use our reader to read an integer from the user and store it in x. Then, we can declare and initialize an array named a, using the value stored in x as the size of the array.

Filling the Array

Next, we’ll enter a for loop that reads input from the user, and then places that input into the array we created. Here’s what that might look like in Java:

int x = Integer.parseInt(reader.nextLine());
int[] a = new int[x];

for(int i = 0; i < x; i++){
  int y =Integer.parseInt(reader.nextLine());
  a[i] = y;
}

Here, we are using a standard For loop to iterate over the array. Inside of the loop, we simply read another input from the user, then store that input into the element in the array indicated by our iterator variable i.

Finding the Sum

Finally, once we’ve filled the array, we must iterate over each element to find the sum and then print that to the terminal:

int x = Integer.parseInt(reader.nextLine());
int[] a = new int[x];

for(int i = 0; i < x; i++){
  int y = Integer.parseInt(reader.nextLine());
  a[i] = y;
}

int sum = 0;
for(int j : a){
  sum += j;
}

System.out.println(sum);

Here, we are using Java’s version of a For Each loop, known as an Enhanced For loop, to iterate over each element stored in the array a. The syntax for an Enhanced For loop in Java is very simple. Inside of the parentheses, we must provide a variable to use as an iterator. Typically we just declare a new variable there, such as int j in this example. Then, following a colon, we give the array that we’d like to iterate over.

Enhanced For Loops are Read Only!

When using an Enhanced For loop in Java to iterate over an array, the array itself cannot be changed from within the loop. If that happens, the Java program will throw an error and stop.

So, we must make sure we don’t try to edit the array from within an Enhanced For loop. Instead, we would use a standard For loop to iterate over the indices of the elements in the array, as seen in the first loop in this example. Using that approach, we can edit the array from within the loop.

Once inside of the loop, we can use j to refer to the current element in a that we are looking at. Notice that it is not the index of that element, but the actual value of the element itself.

So, we can simply add the value of j to the sum variable. Once the loop has terminated, we can just print the sum variable to the terminal.

Two Approaches

This example shows both ways we can use a loop to iterate over the elements in an array. The first approach, using a standard For loop, will iterate over the indices of the array, from 0 up to the length of the array. The second approach, using an Enhanced For loop, iterates over the items themselves, but we cannot edit the array from within the loop.

In general, it is recommended to always use an Enhanced For loop whenever we wish to iterate over an array without making changes to the array itself. However, if we plan on making changes, we should use a standard For loop and iterate over the indices instead.

Reference: The for Statement

Array Subgoals

Working with arrays in Java can also be quite complex. Thankfully, we can break each step down into subgoals to make it more manageable.

Evaluating Arrays

Here are the subgoals we can use to evaluate statements with arrays:

1. Set up Array from 0 to Size - 1

This may be a bit confusing at first, but it will really help us understand arrays. Anytime we are tracing code and see a new array defined, we need to set up an array in our list of variables to store those values. That array will have indexes ranging from 0 to one less than the size of the array. So, if the array has size 5, then we’ll need to make an array variable with 5 boxes, and then label them from 0 to 4. We’ll use that to keep track of the values in each element of the array.

2. Compare Data Type of Statements using Array

Next, we’ll need to compare the data types of any statements using the array. This links back to the earlier subgoals we learned for evaluating expressions. For example, if the array’s data type is int[], we must make sure that each element is treated as an int variable.

3. Trace Statements & Update Slots

Finally, we can trace through the code one line at a time, and update the values of each element in the array as we go. Once again, we’ll need to rely on earlier subgoals to evaluate each expression, conditional construct, and loop that uses the array.

Writing Arrays

We can also use a few subgoals to help us create new arrays

1. Data Type Plus []

To create a new array, we simply determine the data type of each element in the array, and then add square brackets after the data type to create the array data type. So, for an array of double values, we’d declare a variable with type double[].

2. Initialize with {Initializer List} or new datatype[size]

Next, we can initialize the array in one of two ways. If we already know what values we want to store in the array, we can use a list of comma separated values inside of curly braces, as in this example:

int[] a = {1, 2, 3, 4, 5};

Alternatively, if we don’t know the individual elements but we do know the expected size, we can create an empty array of a particular size using this syntax:

int[] a = new int[5];

That’s it! Once we’ve initialized the array, we can treat each element in the array as an individual variable and use the subgoals for evaluating expressions to handle assigning and using values stored in the array.

A Worked Example

Now that we’ve learned how to work with loops and arrays, let’s see if we can solve a more challenging problem using loops and arrays.

Problem Statement

For this example, let’s build a program that matches the following problem statement:

Write a program to calculate weighted grades for a student taking a course. The program should begin by accepting the student’s name and a single positive integer as keyboard input, giving the number of assignments in the course. If the input is invalid, simply print “Invalid Input!” and terminate. All input is from the keyboard.

The program should accept the test scores, as ints between 0 and 100, and the test weights, in order, as floats between 0. and 1. The input pattern will be: test 1, test weight, test 2, test weight 2, …

If an out of range score or weight is input, the program should simply print “Invalid Input!” and terminate. If too many tests are entered, print “Invalid Input!” and terminate.

The program should ensure that the total sum of the weights is equal to 1.0. If the weights do not add to 1.0, The program should print “Invalid Input!” and terminate.

The program should print out the the student’s final score. For each assignment, multiply the score and the weight, and then add that value to the total score for the student. The print out should be of the form <name> earned a ##.## or Trace earned a 78.86 . Use String.format("%s earned a %5.2f",<name>, <final_score>) to create the string for printing.

Class Diagram

We’ll start by roughing out a Driver and Object UML class diagram. Since instances are about the data, lets put all the data and logic about the student and exams in a Student class. Next lets describe the methods which might be useful access and manipulate that data.

  • The attributes would include a student’s name and arrays of their exam scores and and weights. To make arrays we will need to track the “maximum” number of test for a student and the current number of tests stored in the arrays.

  • The methods would include

    • a constructor that accepts as parameter’s the name and maximum number of tests.
    • a method to input a single tests score and weight, with a way to signal if some invalid input has happened
    • a method to check if the test weights sum to 1
    • a method to calculate the final score
    • a toString method that returns <name> earned a ##.##

Let’s have the input method return true if everything with reading and storing the data goes ok, and false if any invalid data is used.

We’ll actually run this program with a Driver class, which will perform the following actions:

  • takes input for the name and number of tests
  • creates a Student object
  • calls the appropriate methods the right number of times to:
    • enter the correct number of scores and weight
    • print out the final score as indicated above
  • If any Student method indicates invalid input was sent, prints “Invalid Input!” and terminates the program

Student-Driver UML Student-Driver UML

The Student Class

First, we’ll need to build the skeleton of our class. By convention, all fields come before all methods and constructors come before other methods.

public class Student{

    // Instance fields
    public String name;
    public int numTests;
    public int maxTests;
    public int[] testScores;
    public double[] testWeights;

    // Constructor(s)
    public Student (String name, int maxTests){

    }

    // Instance Methods
    public boolean addTest(int score, double weight) {
        return false;
    }

    public boolean sumWeights(){ 
        return false;
    }

    public double calcGrade() { 
        return 0.0;
    }

    public String toString() {
        return "";
    }
    
}

This class definition, will compile without errors. Note how each method with a return type returns some default value. Each method has the correct signature and this skeleton should pass any structure test.

Start with Constructor

We know the method Student(name: string, tests: int) on the UML is the constructor. It needs to set up all the instance attributes, of which there are five. It is common in constructors to use parameter names that match instance field names. This necessitates the use of the key word this in the constructor to disambiguate the identifiers.

    public Student (String name, int maxTests){
        this.name = name;
        this.numTests = 0;
        this.maxTests = maxTests;
        this.testScores = new int[maxTests];
        this.testWeights = new double[maxTests];
    }

Here we have initialized the two arrays to the correct size.

Test Each Method As You Go!

A test for this constructor in the Driver class might be

public static void main(String[] args){
    Student test = new Student("Willie", 3);
    System.out.println(test.name);
    System.out.println(test.maxTests);
    System.out.println(test.numTests);
    System.out.println(test.testScores.length);
    System.out.println(test.testWeights.length);       
}

You should be able to determine the values which should print from the code.

AddTest Method

First we need to check a few things when arriving in the addTest method. First, we need to make sure the arrays are not at full. This is the purpose of the numTests attribute. The numTests attribute starts at 0, and is incremented each time a test is added. Thus as long as numTests < maxTests, there is still room in the array of another test.

If there is room, we need to ensure that the parameters are in range. If we see a problem in any of these areas, return false. Assuming the parameters are in range, add them to the arrays and increment numTests.

Finally, if the list of weights is now full, we need to check that it sums to 1.0. We have a method, sumWeights() which performs this check, so we just call it.

    public boolean addTest(int score, double weight) {
        // check that we can add another test
        if (numTests < maxTests) {
            // check if score and weight are valid
            if (score < 0 || score > 100 || weight < 0.0 || weight > 1.0) {
                // return false if an error occurs
                // the Driver class will print the error
                return false;
            }
            // add the score and weight to the arrays
            testScores[numTests] = score;
            testWeights[numTests] = weight;
            numTests++;
            // check if arrays are full
            if (numTests == maxTests) {
                // if so, return an error if the sum is invalid
                return sumWeights();
            }
            // everything is good, so return true
            return true;
        } else {
            // cannot add a test, so return false
            return false;
        }
    }

In our Driver class, we might call this method in this way:

Student student = new Student("Willie", 3);
if (!student.addTest(80, 0.25)){
    System.out.println("Invalid Input!");
    return;
}

If there is a problem in processing the parameters, the addTest() method should return false. So, if the method returns true no error will be printed, but if it returns false then we will enter the if-statement and print an error instead.

SumWeights Method

This method is a great example of the accumulator pattern when working with arrays. In the accumulator pattern we typically use an enhanced for loop (a for each loop) to iterate through each element in the array/. Inside of the loop, we compute some value across all of those array elements, such as the sum, maximum, or average.

Finally, recall that double data types are subject to some minor representation error, so when we test to see if the value is exactly 1.0 we should really check if it is within a very small range of values between 0.99999999 and 1.000000001 or similar.

    public boolean sumWeights(){
        double sum = 0.0;
        for(double d : testWeights){
            sum += d;
        }
        // check if sum is very close to 1.0
        return 0.99999999 < sum && sum < 1.000000001;
    }

CalcGrade Method

Here we use the accumulator pattern again to sum up the contribution of each test (testScores[i] * testWeights[i]). We can do this because we entered the scores and weights in order; so testScores[0] is matched with testWeights[1]. However, since we are using two different arrays, we cannot use an enhanced for loop, and must instead us a standard for loop, as we’ll see in the code.

This practice is called the parallel array pattern. Each array has different type of data, but each matching index in every array is related in some way. This is a fairly common pattern in non-object-oriented programming. Later in this class, we’ll see how to use our own classes to better store this data in an object-oriented way.

    public double calcGrade() {
        double sum = 0.0;
        for (int i = 0; i < numTests; i++) {
            // multiply the score by the weight and add to total
            sum += testScores[i] * testWeights[i];
        }
        return sum;
    }

ToString Method

Here we use the given string format String.format("%s earned a %5.2f",<name>, <final_score>) to create our output. So, we just compute the final grade and return that string:

    public String toString(){
        double sum = calcGrade();
        return String.format("%s earned a %5.2f", name, sum);
    }

At this point, our Student class should be complete. Feel free to stop a minute and test it by writing your own code in the Driver class before moving on.

Driver Class

The Driver class should perform the following steps:

  • take input for the name and number of tests
  • create a Student object
  • call the appropriate methods the right number of times to:
    • enter the correct number of scores and weight
    • print out the final score as indicated above
  • If any Student method indicates invalid input was sent, print “Invalid Input!” and terminate the program

Since the Driver class only has a main method, we can start with this skeleton:

public class Driver{

    public static void main(String[] args) {

    }
}

From there, we need to add the code to create a Scanner object to read input from the terminal. This involves importing the java.util.Scanner library, and then instantiating a Scanner inside of the main method to read from System.in:

import java.util.Scanner;

public class Driver{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

    }
}

Next, we can start by reading the name of the student and the number of tests from the terminal, and then instantiating our new Student object using that data:

import java.util.Scanner;

public class Driver{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter the student's name: ");
        String name = scanner.nextLine();
        System.out.print("Enter the total number of tests: ");
        int maxTests = Integer.parseInt(scanner.nextLine());
        Student s = new Student(name, maxTests);

    }
}

Now that we have created our student, we can read the input for each score and weight and slowly populate that Student object using that information. If any of those methods returns false we know that we’ve encountered an error and have to stop the program.

import java.util.Scanner;

public class Driver{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter the student's name: ");
        String name = scanner.nextLine();
        System.out.print("Enter the total number of tests: ");
        int maxTests = Integer.parseInt(scanner.nextLine());
        Student s = new Student(name, maxTests);
        for (int i = 0; i < maxTests; i++) {
            System.out.print("Enter score " + (i+1) + ": ");
            int score = Integer.parseInt(scanner.nextLine());
            System.out.print("Enter weight " + (i+1) + ": ");
            double weight = Double.parseDouble(scanner.nextLine());
            if (!s.addTest(score, weight)) {
                System.out.println("Invalid Input!");
                return;
            }
        }

    }
}

Finally, if we’ve entered all of the scores, we can just call the toString method of the Student class to print the final score earned by that student:

import java.util.Scanner;

public class Driver{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter the student's name: ");
        String name = scanner.nextLine();
        System.out.print("Enter the total number of tests: ");
        int maxTests = Integer.parseInt(scanner.nextLine());
        Student s = new Student(name, maxTests);
        for (int i = 0; i < maxTests; i++) {
            System.out.println("Enter score " + (i+1) + ": ");
            int score = Integer.parseInt(scanner.nextLine());
            System.out.println("Enter weight " + (i+1) + ": ");
            double weight = Double.parseDouble(scanner.nextLine());
            if (!s.addTest(score, weight)) {
                System.out.println("Invalid Input!");
                return;
            }
        }
        System.out.println(s.toString());
    }
}

A sample execution of this program might look like this:

Student Test Execution Student Test Execution

Subsections of A Worked Example

A 2D Worked Example

Let’s do one more example, this time with multidimensional arrays using Matrix.java and MatrixDriver.java.

Problem Statement

Let’s see if we can write a program that solves this problem statement:

Write a program to print the transposition of a two dimensional array. Begin reading two numbers, m and n, as input from the keyboard, with m representing the number of rows in the array and n representing the number of columns. If either input is 0 or less, output “Error - Invalid Input!” and terminate the program.

Otherwise, the program should then read m * n integer inputs from the keyboard and place them into the array, filling up each row before moving to the next row.

Once all inputs have been accepted, the program should print the transposed version of the array, where the orientation is adjusted so that row i is now column i and column i is row i.

Let’s see if we can build a driver and object class program that meets that specification.

UML Class Diagram

The data is simply an m * n 2-dimensional array, or a m * n matrix. The Matrix class should know its basic dimensions, it should have a way to fill up the matrix and a way to print out its transpose. This class will need access to the keyboard.

The MatrixDriver should take keyboard input, instantiate a Matrix object, fill it up and then call the object’s print transpose method.

Matrix-Driver UML Matrix-Driver UML

Matrix Class

Lets set up the following methods:

  • Matrix(int r, int c): lets have the constructor take the number of rows a columns as parameters AND build an empty r * c int array filled with 0.
  • fillData(): lets write a function that accepts keyboard input and fills up the matrix, row-by-row, column-by-column.
  • printTranspose(): print out the transpose of the matrix, or the rows and columns switched.

The transpose and fill operations can be tricky. As always, start with a sketch of their control flow, paying attention to the types of loops you may want to use as well as the number of arrays you will need. Don’t forget to import java.util.Scanner.

Start by declaring Matrix’s instance attributes

public int rows;
public int columns;
public int[][] matrix;

Matrix()

Our constructor should accept two integer arguments, rows and columns and initialize three instance attributes.

public Matrix(int rows, int columns){
    this.rows = rows;
    this.columns =  columns;
    matrix = new int[rows][columns];
}

fillData()

This method should accept m * n keyboard inputs and place them in the matrix. It actually makes sense to use multiple nested For loops, one for each dimension of the array. Consider the following code:

Scanner scanner = new Scanner(System.in);
for(int row = 0; row < this.rows; row++){          // each row
    for(int col = 0; col < this.columns; col++){   // each column or element
        matrix[row][col] = scanner.nextInt();      //get keyboard input
    }
}

In this code, we have one For loop that loops through each row in the list; here row is the index of the row . Then, inside of that loop, there is a second For loop using col to march through each column (element) of that row.

Inside of that loop, we can use row and col as indices to access a particular element in our list. In this way, we’ll fill up the list, starting with the entire first row, then the second row, and so on.

printTranspose()

Finally, we want to print the transposed version of this list. As it turns out, it is very simple to do that by changing the order of our For loops:

for(int col = 0; col < this.columns; col++){
    for(int row = 0; row < this.rows; row++){ 
        System.out.print( matrix[row][col] + " ");
    }
    System.out.println();
}

We iterate first through the columns, then through the rows. In this case, we will print each item in the first column from each row, but they will be printed on a single line, separated by spaces. So, in this way, the first column will be displayed as a row. Then, we’ll go to the next column, and print it as a single row.

Finally, notice the inclusion of an empty println after the innermost For loop, but still inside the other loop. This will simply end the line of output for the current column by printing a new line to the terminal.

The Driver

The driver should take command line arguments as input, instantiate a Matrix object, fill it up and then call the object’s print transpose method. The number of rows will be the first command-line argument, and the number of columns will be the second command-line argument.

The skeleton of MatrixDriver might look like

import java.util.Scanner;

public class MatrixDriver{
    
    public static void main(String[] args){
        // read a number of rows and columns from the keyboard
        // check if the numbers are valid - if not print an error and quit
        // create a matrix object of that size
        // fill the matrix with data
        // print the transposed matrix
    }
}

The rest is straight forward and left for you to do. The Driver class is not graded for this example.

Summary

As we’ve learned in this chapter, aggregate data types, like arrays and lists, are a ways to store data in a program. Arrays or lists allow us to store a large amount of information in a single variable, using indexes and multiple dimensions to arrange the data in a logical fashion.

We can then combine those arrays with loops to create powerful programs that can access all of the data stored in arrays.

Finally, there are many different built-in operations each programming language allows us to perform on arrays and lists, such as finding their size, copying elements, and more.

Next, we’ll see how to put these concepts into practice by building a fun game in the project for this module!