Skip to content

Reading 19: Java, part 2: classes and methods

I enjoy evangelizing Java. In my heart of hearts, I'm an engineer, and what makes me happy is building something that works and having someone use it. That's cool.
- James Gosling (inventor of Java)

Overview

In the previous reading we saw how to create a very simple Java program, and we saw a bit of Java's structure and syntax. In this chapter, we will write a more realistic program that prominently features Java's primary object-oriented features: classes and methods.

The problem

The problem we are going to use Java to solve is quite simple. Given a file of numbers (one number per line), write a program that will read the numbers from the file, and print out statistics of the numbers.

We will build up this program starting from an extremely sparse "shell" of a program which does almost nothing, and ending up with a fairly full-featured program.

Our program will go into a source code file called Stats.java, and the class we are defining will be called Stats.

Stats.java, version 0

Here is a very trivial shell of this class we can use to get started:

import java.util.ArrayList;

public class Stats {
    /** Stored numbers. */
    private ArrayList<Double> contents;

   /**
    * Run the program.
    *
    * @param args  the array of command-line arguments
    * @return      nothing
    */
    public static void main(String[] args) {
        Stats s = new Stats();
    }

    /** Make a new Stats instance. */
    public Stats() {
        // do nothing
    }
}

We can actually compile and run this code! Let's assume we have a file of numbers called nums. Type the following:

$ javac Stats.java
$ java Stats nums

Nothing happens! But don't panic — we're just getting started!

Fields

The first thing we are going to focus on is this line:

private ArrayList<Double> contents;

This comes inside the Stats class definition. It is called a field of the class. That means it's part of the data that objects created from the class have access to. It also has the private access modifier, which means that it can only be accessed by methods of this class. And since it isn't static, it can't be accessed inside a static method like main, either, but only inside "regular" methods of the class. (We'll make some of those soon.) The name of the field is contents, and its type is ArrayList<Double>.

The name contents suggests that it is going to be used to hold something. In this case, it's going to contain the numbers that have been read in from a file. The type of this field (ArrayList<Double>) is a bit more complicated. Recall that Python has "lists", which are what other languages usually call "arrays", i.e. a sequence of values that can be accessed given the location ("index") of the values in the list. Also, you can add elements to the end of a Python list using the append method. Well, that's basically what the contents field is, but with this restriction: it can only hold double-precision floating-point numbers (which is what the Double part means). Java calls this an ArrayList because

  • it represents a list of values
  • it behaves like an array in other languages
  • there are other kinds of lists (e.g. LinkedList)

An ArrayList is a full-fledged Java object. Confusingly, Java also has a more primitive kind of array. We see one of those in the argument to the main method:

public static void main(String[] args) {

The args argument has type String[], which means it's a (primitive) array of Strings. Primitive arrays are OK if you never have to resize the array, but we will be adding numbers to the array quite frequently, so we use an ArrayList (which lets you do this) instead of a primitive array (which doesn't).

Also, the syntax ArrayList<Double> is interesting. It states that this particular ArrayList can only contain Doubles. Python lists can contain any Python value, but Java is much more restrictive. The Java feature that this represents is called generics, which means that one data structure (the ArrayList) can be specialized to contain a particular type only (the <Double> part). Adding the <Double> to the ArrayList type name guarantees that we can't add anything but a Double value to the ArrayList.

Note also that in order to use the ArrayList class, we have to add the line

import java.util.ArrayList;

before the Stats class definition. This "imports" the ArrayList class from a java "package" called java.util. If you forget to do this, the code won't compile!

Constructors and Instances

All the contents field does is describe what the field is going to be once an instance of the Stats class is created. But how do we create instances of this class? And what's an "instance" anyway?

"Classes", in object-oriented programming terminology, are a "template" or "specification" of how to build objects. An "instance" is an object created from this template. Here, we would say "an instance of the class Stats" to mean an object which was created from this class. Any such object contains all the fields defined in the class, including contents, in this case. Instances also have the ability to call "methods" on themselves, which we will see shortly.

OK, so now we know what instances are. How exactly do we make one? We use a constructor method (constructor for short). This is a special method whose name is the same as the name of the class. Here, it's pretty simple:

public Stats() {
    // do nothing
}

Constructors can take arguments, but this one doesn't. It also (currently) doesn't do anything, which we will fix shortly. The constructor is called from the main method, where it's used to create a new Stats object:

public static void main(String[] args) {
    Stats s = new Stats();  // call the constructor
}

So what this version of the program does is:

  • Create a new Stats object, calling it s,
  • and nothing else!

Clearly, there has to be more that we can do. So let's get to it!

Stats.java, version 1

The constructor again

The first thing we will do is make the constructor do something useful. You might think that because we declared the contents field in the line:

private ArrayList<Double> contents;

that the contents array is ready to use. Wrong! If we tried to use it, we would get what is known as a NullPointerException. The reason for this is that the above line is simply a declaration of the field; it tells us that the field exists, and has a particular type, but it doesn't actually create the contents object. Instead, the contents field is initialized to a special value called null, which basically means "the absence of an object". Creating fields is one of the things that is often done inside of a constructor, so that's what we'll do. We'll add one line to our constructor:

public Stats() {
    contents = new ArrayList<Double>();  // make the `contents` array
}

What this does is initialize the contents array with a new ArrayList<Double> object. The new keyword is followed by a call to the ArrayList<Double> constructor, and the arguments (if any) to the constructor go inside the parentheses. Here, there are no arguments, which causes the ArrayList<Double> constructor to create an empty array. It gets assigned to the contents field, which thereafter contains the array.

The new keyword

The new keyword can't be left out! Every time you want to create an object by calling a constructor, you need to use new. (In Python, there is no need for this, and new isn't even a keyword.)

The contents field can be accessed and changed inside any (non-static) method of the class. (That's why we could initialize it in the constructor.) Fields act a bit like global variables, in that they aren't local to any function (method), but unlike global variables, they're restricted to methods in the class.

Command-line arguments

Now that the constructor is actually creating an array we can use, we can add more stuff to the main method. Previously, it was just this:

public static void main(String[] args) {
    Stats s = new Stats();
}

This just creates a Stats object, but doesn't do anything with it. What we want the Stats object to do is to read numbers from a file, and then compute some statistics from them.

The first thing we will need is the name of the file. We can use the String[] args argument to main to get this name. We will assume that the person who runs this program will put the filename on the terminal command line when they run the program, like this:

$ java Stats nums

Here, nums is the name of the file of numbers which we (hopefully) have in the same directory. When we invoke the program like this, any words that come after the java Stats part are command-line arguments and are put into the args argument of the main method as an array of strings. Here, the args array only has one string: nums. This is found in the array at index 0, so it's args[0]. We could just go ahead and start using args[0] as the name of the file, but beware! There is nothing that forces the user of the program to input a filename. If they don't, then accessing args[0] will cause the program to throw an ArrayIndexOutOfBoundsException, which will crash the program. That isn't nice, so instead we will check to make sure that args has at least one argument. While we're at it, we might as well check that it has exactly one argument, since extra arguments aren't needed and should be considered to be an error.

That leads us to extend the main method into this:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
}

The args array has one field, called length, which contains the length of the array. We expect that it will be exactly 1 (containing just the name of the file that contains the numbers), so if it isn't, we do two things:

  1. Print an error message.
  2. Exit the program.

The error message we are printing is called a usage message, which is a particular kind of error message which says "you didn't run this program with the correct arguments; here's the way you should have done it." This is the line:

System.err.println("usage: java Stats filename");

If you run this program with no arguments, or with more than one argument, you will see this:

$ java Stats
usage: java Stats filename

which tells you that you should have called the program with exactly one argument, which should be the name of a file. This helps anyone running the program incorrectly to know how they should have run the program.

Usage messages are just a convention. You can write any kind of error message you want, but experience has shown that a well-written usage message conveys a lot of information in a small space. Writing good usage messages is beyond the scope of this reading, but the usage message given above is sufficient for the program we're writing here.

Note that we print the usage message to System.err, not to System.out. System.err is a PrintStream object, like System.out, but it is intended to be used only for error messages. By default, they will print to the terminal as well, but it's possible to redirect them to somewhere else (e.g. a log file). As a general rule, you should print error messages to System.err and not System.out.

After printing the usage message, we exit the program by calling the System.exit method:

System.exit(1);

System.exit is a method that exits the program. The 1 argument is a convention; when you want to exit a program after an error, you use an argument of 1. If you use an argument of 0, it means the program exited successfully. (However, usually in that case you don't bother calling System.exit at all.) The actual argument of System.exit (0, 1, or something else) is given back to the operating system, which can do whatever it wants with that information. Usually, it's ignored, but there are programs that invoke other programs, such as build systems, that can use this information.

Assuming that there was exactly one argument, it gets stored in a variable called filename. Then we create a new Stats object by calling the Stats constructor:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
}

However, we aren't using the filename for anything yet. Let's fix that!

The load method

For now, let's assume that nums has the following contents:

1.2
2.3
3.4
4.5
5.6

We want to load the numbers in the nums file into the program. To do this, we will define a method called load, which will take the filename as an argument:

public void load(String filename) {
    // TODO
}

We use the // TODO comment to indicate that we still have to write the body of this method.

Stubs

Methods like this are often called "stubs"; they contain the correct name, arguments, and return value, but they have no body and do nothing. They are placeholders which are meant to be filled in later.

Even though we haven't written the body of the load method yet, we can still update our main method to call it:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    s.load(filename);
}

We use the new Stats object, called s, and call the load method of that object, using the filename as its only argument. Now we just have to write the body of the load method. Here is a first version of this method:

public void load(String filename) {
    Scanner sc = new Scanner(new File(filename));
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        double d = Double.parseDouble(line); 
        contents.add(d);
    }
    sc.close();
}

There is a bunch of new stuff here!

Reading input from files is more complicated in Java than it is in Python. In Python, you can just use the readline() method on a file to read in a line from a file:

line = file.readline()

and you are done. Java is more general than Python, but it's also more complicated. Let's look at the first line:

Scanner sc = new Scanner(new File(filename));

We are creating a Scanner object called sc. This is an object that can read various types of values from input sources, including files. In order to call the Scanner constructor, we need a file as its argument. But we don't have a file; we have a file name (a string!). In Java, like in Python, a file is an object. So we have to create a file object using the File constructor; that's what new File(filename) does. In fact, we could have split this into two lines:

File file = new File(filename);  // create the file object
Scanner sc = new Scanner(file);  // create the Scanner from the file

We do it in one line because we don't need the file object for anything else.

Once we have the Scanner object sc, we are going to use two of its methods:

  1. hasNextLine
  2. nextLine

The hasNextLine method returns a boolean value which is true if there are any unread lines in the file, and false if not (all the lines of the file have been read). The nextLine method reads the next line of the file and returns the line (without the newline at the end of the line) as a String. So this while loop:

while (sc.hasNextLine()) {
    String line = sc.nextLine();
}

would just read every line in the file until there were no more lines, assigning each line to the variable called line. This wouldn't be very useful; we need to convert the lines to numbers! In order to do this, we can use a utility method of the Double class called parseDouble. It takes in a String and tries to convert it to a double, which is a double-precision floating-point number. Not all Strings can be converted to doubles, of course; the string "foobar" certainly can't, for instance. In that case, the parseDouble method throws a NumberFormatException exception. But if the file does contain numbers, we can read them in and add them to the end of the contents array using the contents.add method:

while (sc.hasNextLine()) {
    String line = sc.nextLine();
    double d = Double.parseDouble(line); 
    contents.add(d);
}

After there are no lines left unread in the file, we are done with this method. One last thing we should do is "close" the scanner. This will make sure that we don't try to read from it again when there is nothing left to read. We use the close method to do this:

public void load(String filename) {
    Scanner sc = new Scanner(new File(filename));
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        double d = Double.parseDouble(line); 
        contents.add(d);
    }
    sc.close();
}

This method is almost usable. The only problem is that it won't compile! If we do try to compile it, here's what we get:

Stats.java:32: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
        Scanner sc = new Scanner(new File(filename));
                     ^
1 error

Java is very picky about exceptions. In this case, the File constructor is trying to create a file given a filename, but it can only do this if there is a file with that name available! If not, the method throws an exception called FileNotFoundException. This is probably not surprising. What is surprising is that Java wants you to annotate the method by saying, in effect, "By the way, this method I'm about to define can throw a FileNotFoundException." We do this by adding a throws clause on the first line of the method:

public void load(String filename) throws FileNotFoundException {
    Scanner sc = new Scanner(new File(filename));
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        double d = Double.parseDouble(line); 
        contents.add(d);
    }
    sc.close();
}

Now, any other part of the code that uses this method has been warned that the method can throw a FileNotFoundException, and can take the appropriate actions. (We'll see shortly what those are.)

It turns out that there is one other exception that can be thrown inside this method. The Double.parseDouble method may fail to convert a String to a Double. If so, it will throw a NumberFormatException. For very arbitrary reasons, we don't have to declare that the load method can throw this exception, but we probably should. That leads to this:

public void load(String filename) throws FileNotFoundException, NumberFormatException {
    Scanner sc = new Scanner(new File(filename));
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        double d = Double.parseDouble(line); 
        contents.add(d);
    }
    sc.close();
}

This method is now fine, but the file as a whole will still not compile. We get this:

$ javac Stats.java
Stats.java:22: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
        s.load(filename);
              ^
1 error

Now it's the main method that has to indicate that it can throw the FileNotFoundException! We can add a throws clause to main as well:

public static void main(String[] args) throws FileNotFoundException {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    s.load(filename);
}

However, this is a bad idea. If we invoke the program with a filename which doesn't correspond to a real file, the call to the load method will throw a FileNotFoundException, the exception will propagate back to the main method, and since the main method isn't handling it, it will crash the program, printing a traceback which looks like this:

$ java Stats foo
Exception in thread "main" java.io.FileNotFoundException: foo (No such file or directory)
        at java.base/java.io.FileInputStream.open0(Native Method)
        at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
        at java.base/java.io.FileInputStream.<init>(FileInputStream.java:159)
        at java.base/java.util.Scanner.<init>(Scanner.java:662)
        at Stats.load(Stats.java:32)
        at Stats.main(Stats.java:22)

This is ugly and confusing. (Java is famous for its complex and hard-to-understand tracebacks.) If you read the first line, you can probably figure out that the program was trying to read a nonexistent file called foo, but it could certainly have been expressed in a more readable way! The way to do this is to catch the exception and print out a decent error message. That leads to this version of the main function:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    try {
        s.load(filename);
    } catch (FileNotFoundException e) {
        System.err.println("ERROR: " + e.getMessage());
        System.exit(1);
    }
}

We added the try/catch construct to call the load method. What this means is that we try to call the load method. If it succeeds, great. If it fails because it threw a FileNotFoundException, then the catch block "catches" this exception, which it calls e. It then executes the code inside the catch block. What this does is extract the error message from the exception e by calling e.getMessage(). It prints the error message to System.err (since it's an error message, we don't print to System.out), and calls System.exit(1) to exit the program.

Exception handling

This is by no means the only way we could handle this exception. See the previous readings on exception handling for some alternatives.

Once we have done this, we can actually compile and run the program:

$ javac Stats.java
$ java Stats foo
ERROR: foo (No such file or directory)
$ java Stats nums

When we gave the name of an existing file that contains numbers (here, nums), the contents of the file are loaded into the object. Unfortunately, we didn't tell it to do anything with these numbers, so no output is printed! We'll fix that momentarily, but first, we have to deal with one other potential error scenario.

As mentioned above, if we read in a line from a file which can't be converted to a number (or to be more precise, a double), the Double.parseDouble method throws a NumberFormatException exception. Although Java doesn't require that we say that methods that throw this exception do so (unlike FileNotFoundException), it can still happen and it can still crash the program. Let's change the nums file so that the file now looks like this:

1.2
2.3
three_point_four
4.5
5.6

Obviously, the third line can't be converted to a double. Let's run the program again:

$ java Stats num
Exception in thread "main" java.lang.NumberFormatException: For input string: "three_point_four"
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:944)
        at Stats.load(Stats.java:45)
        at Stats.main(Stats.java:23)

Oops, we got another crash! Even though a traceback does give information about where an error occurred (it apparently occurred at line 23 of Stats.java, which is where the called to s.load(filename) was), this isn't something an end user should see. Instead, we simply have to add another catch block in the main method to handle the case where calling the load method throws the NumberFormatException exception:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    try {
        s.load(filename);
    } catch (FileNotFoundException e) {
        System.err.println("ERROR: " + e.getMessage());
        System.exit(1);
    } catch (NumberFormatException e) {
        System.err.println("ERROR: " + e.getMessage());
        System.exit(1);
    }
}

The new catch block will be executed in the event that the s.load call in the try block results in a NumberFormatException. It will print out the exception message and exit, just as it did for the FileNotFoundException case.

If we run it on the bad nums file, we get this:

$ java Stats nums1
ERROR: For input string: "three_point_four"

This identifies the invalid line, but the error message could be a bit better. Let's clean that up a bit:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    try {
        s.load(filename);
    } catch (FileNotFoundException e) {
        System.err.println("FILE NOT FOUND: " + e.getMessage());
        System.exit(1);
    } catch (NumberFormatException e) {
        System.err.println("BAD NUMBER FORMAT: " + e.getMessage());
        System.exit(1);
    }
}

Now, when we recompile and re-run the program, we get this:

$ java Stats nums1
BAD NUMBER FORMAT: For input string: "three_point_four"

And with that, we are finally done with the load() method! 😄

The dump method

Implementing the load method was pretty annoying, but after that, everything will be smooth sailing.

For this version of the program, we just want some way to make sure that the numbers we read in from the file are actually there. In order to do this, we will implement a method called dump that does nothing but print the numbers to the terminal. Here is this method in its entirety:

public void dump() {
    for (int i = 0; i < contents.size(); i++) {
        System.out.println(contents.get(i));
    }
}

There isn't much to it. It goes through the contents array by index, starting with index 0 and going up to contents.size() - 1 (since i < contents.size()). Obviously, contents.size() is the size of the array i.e. the number of numbers in the array. For each index, the contents.get(i) method is called to get the element of the array at that index, and the number is printed using System.out.println.

Note

One peculiarity of using ArrayLists is that, unlike primitive arrays, we can't use the nice square bracket syntax to get elements of the array. So we have to say contents.get(i) instead of contents[i], which would be a lot nicer to read!

In Python, you can do this sort of thing because Python lets you define what the square bracket indexing syntax means for any array-like class that you implement. Java doesn't allow this (and Java programmers have been complaining about this ever since the language came out). Technically speaking, Java does not support operator overloading, and here the "operator" is the square bracket syntax. So we have to say contents.get(i).

Now that we have the dump method, we have to add it to main so that it gets called:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    try {
        s.load(filename);
        s.dump();
    } catch (FileNotFoundException e) {
        System.err.println("FILE NOT FOUND: " + e.getMessage());
        System.exit(1);
    } catch (NumberFormatException e) {
        System.err.println("BAD NUMBER FORMAT: " + e.getMessage());
        System.exit(1);
    }
}

When we compile run this program on our original nums file (the one without the errors), we get this:

$ java Stats nums
1.2
2.3
3.4
4.5
5.6

That doesn't look like much, but now we know that our program is reading the file and all the numbers are getting into the contents array where they belong.

That ends this version of the program.

Stats.java, version 2

OK, now we have a program that can read numbers from a file and print them out. But what we want is to compute statistics on the numbers, and print out those statistics. We will break this down into two methods:

  • computeStats
  • printStats

You might wonder why we don't just do everything in one method. In fact, computing the statistics and printing the statistics are two different things. You could imagine situations where we wanted to compute the statistics but didn't need to print out the results all at once. (We'll get back to this later.)

The statistics we are going to compute are the following:

  • the sum of the values
  • the minimum value
  • the maximum value
  • the mean value

(Arguably, the sum isn't a "statistic", but we won't worry about that.)

The main method

We need to update the main method to use the new methods that we are going to write:

public static void main(String[] args) {
    if (args.length != 1) {
        System.err.println("usage: java Stats filename");
        System.exit(1);
    }
    String filename = args[0];
    Stats s = new Stats();
    try {
        s.load(filename);
        s.computeStats();
        s.printStats();
    } catch (FileNotFoundException e) {
        System.err.println("FILE NOT FOUND: " + e.getMessage());
        System.exit(1);
    } catch (NumberFormatException e) {
        System.err.println("BAD NUMBER FORMAT: " + e.getMessage());
        System.exit(1);
    }
}

New fields

Since these statistics will be computed in one method (computeStats) and used in another (printStats) we will create new fields to hold them:

private double min;   // minimum number
private double max;   // maximum number
private double sum;   // sum of all numbers
private double mean;  // mean of all numbers

These go outside of the methods, like the contents field did.

printStats

Writing the computeStats method will be a bit of work, but we can immediately write the printStats method:

public void printStats() {
   System.out.println("Number of entries: " + contents.size());
   System.out.println("Minimum: " + String.format("%.2f", min));
   System.out.println("Maximum: " + String.format("%.2f", max));
   System.out.println("Sum: " + String.format("%.2f", sum));
   System.out.println("Mean: " + String.format("%.2f", mean));
}

Since min, max, sum, and mean are fields of the object, the printStats method has direct access to them, and can print them out. We also print out the number of entries in the array.

The only novelty here is the String.format method. We use this to guarantee that our numbers get printed out in a particular format which has exactly two decimal places. The %.2f is a format string; this has a long history going all the way back to the C programming language. All you need to know is that %.2f means that the number will be printed out as a floating point number (the f part) and has two decimal places (the .2 part).

computeStats

Now we have to write the computeStats method. It's pretty straightforward:

public void computeStats() {
    min =  1000000000.0;
    max = -1000000000.0;
    sum = 0.0;
    for (int i = 0; i < contents.size(); i++) {
       double curr = contents.get(i);
       sum += curr;
       if (curr < min) {
           min = curr; 
       }
       if (curr > max) {
           max = curr;
       }
    }
    mean = sum / contents.size();
}

Computing the sum is easy: we initialize the sum field to 0.0, go through the entire contents array, and add each number to the sum.

Computing the mean is easy once we have the sum: we just divide the sum by the size of the array. (Recall that the "mean" is just the average of all the numbers.)

Computing the minimum and maximum is a bit trickier because we have to have a starting point for the computation. We initialize the min value to a very large number and the max value to a very small number. This means that any number we actually encounter will be smaller than the initial min and larger than the initial max. However, this isn't ideal: if there are no numbers in the array, we will end up with a very large min and a very small max!

A different approach could be to use the first number of the array as both the min and the max. But in that case, an empty array would crash the program! We could check for that and throw an exception in that case, in which case we would probably have to edit our main function again. This is straightforward but boring, so we'll leave it as an "exercise for the reader".1

Once this method is written, we can recompile the program and run in on our nums file to get this:

$ java Stats nums
Number of entries: 5
Minimum: 1.20
Maximum: 5.60
Sum: 17.00
Mean: 3.40

This program is almost finished, but there is one more thing we would like to add.

Accessors

As currently written, the program can read in a file of numbers and compute and print out statistics on these numbers. That's great, but the Stats class can be used more widely than in just this single program. You might have another program that, as part of various things it does, wants to be able to read numbers from a file and compute statistics on them. For that program, the Stats class would be only one component of the entire program.

In that case, the other program could create a new Stats object, much like is done in the main method given above. It could call load on the Stats object to load numbers from a file. It could call computeStats to compute the statistics on those numbers. It could call printStats to print out those statistics.

But most of the time, the other program wouldn't want to just print out the statistics. It would want to access the statistics for its own purposes. For instance, it might want to know what the mean of the numbers is so it can do something with that value. Currently, we don't have a way to do this. And because the mean field of the Stats class is private, it can't be accessed by any code outside of the class.

One way to deal with this is to make the mean field (and the other fields) public. Then any other program can access them. This is usually a very bad idea! Objects should be self-contained, and allowing any other class's code to mess with the object's internal fields can completely break the object!

For instance, let's say you read some numbers from a file and computed the statistics. Now the mean field should have the average of all the numbers. But if the field is public, any other class' code can change that to a bogus value which no longer represents the mean of the numbers, and there isn't anything you can do about it. So we don't want to make the fields public.

What we really want is a way to make the fields "read-only" by outside code. Unfortunately, Java doesn't have a readonly access modifier, so the traditional way of dealing with this is to write very simple "accessor" methods. We can write them down immediately:

public double size() {
    return contents.size();
}

public double getMin() {
    return min;
}

public double getMax() {
    return max;
}

public double getSum() {
    return sum;
}

public double getMean() {
    return mean;
}

These are trivial methods, but they are useful. Any other program that is using a Stats object and needs the mean of the numbers can just call the public method getMean to get it (and similarly for size, min, max, and sum). However, these other programs can't change the values of any of these fields. Methods like these are usually called accessor methods (or just accessors for short) since they "access" values of fields. It is annoying to have to write out these accessor methods, but it's worth it.

One other thing we would like is to have an accessor for the entire contents array. We could write a similar accessor like this:

public ArrayList<Double> getContents() {
    return contents;
}

However, this is again a very bad idea. This does not copy the contents array, it returns the actual contents array. So any code that receives that array can change it. We don't want that! The solution is simply to make a copy. There are different ways to do that, but we'll do it manually:

public ArrayList<Double> getContents() {
    ArrayList<Double> copy = new ArrayList<Double>();
    for (int i = 0; i < contents.size(); i++) {
        copy.add(contents.get(i));
    }
    return copy;
}

And with that, this program is finished!

The entire source code of the program, with comments, is available here.

Extensions

It's easy to extend the Stats class to do more things. For example:

  • Add a stdev method to compute standard deviations.
  • Add a median method to compute medians.
  • Add a removeOutliers method to remove "outliers", which are values that are more than 5 standard deviations from the mean. (This is pretty tricky to get right!)

These are all (you guessed it!) "exercises for the reader". Have fun!


  1. An "exercise for the reader" means that the authors couldn't be bothered to do it themselves.