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:
Nothing happens! But don't panic — we're just getting started!
Fields
The first thing we are going to focus on is this line:
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:
The args
argument has type String[]
,
which means it's a (primitive) array of String
s.
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 Double
s.
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
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:
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:
So what this version of the program does is:
- Create a new
Stats
object, calling its
, - 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:
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:
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:
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:
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:
- Print an error message.
- 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:
If you run this program with no arguments, or with more than one argument, you will see this:
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
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:
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:
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:
and you are done. Java is more general than Python, but it's also more complicated. Let's look at the first line:
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:
hasNextLine
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:
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 String
s can be converted to double
s, 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:
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:
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:
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:
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 ArrayList
s
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:
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:
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:
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!
-
An "exercise for the reader" means that the authors couldn't be bothered to do it themselves. ↩