Chapter 9. Immutable Objects

Java is an object-oriented language, which means that it uses objects to (1) represent data and (2) provide methods related to them. This way of organizing programs is a powerful design concept, and we will introduce it gradually throughout the remainder of the book.

An object is a collection of data that provides a set of methods. For example, Scanner, which you saw in “The Scanner Class”, is an object that provides methods for parsing input. System.out and System.in are also objects.

Strings are objects too. They contain characters and provide methods for manipulating character data. Other data types, like Integer, contain numbers and provide methods for manipulating number data. We will explore some of these methods in this chapter.

Primitives Versus Objects

Not everything in Java is an object: int, double, char, and boolean are primitive types. When you declare a variable with a primitive type, Java reserves a small amount of memory to store its value. Figure 9-1 shows how the following values are stored in memory:

int number = -2;
char symbol = '!';
Figure 9-1. Memory diagram of two primitive variables

As you learned in “Accessing Elements”, an array variable stores a reference to an array. For example, the following line declares a variable named array and creates an array of three characters:

    char[] array = {'c', 'a', 't'};

Figure 9-2 shows them both, with a box to represent the location of the variable and an arrow pointing to the location of the array.

Figure 9-2. Memory diagram of an array of characters

Objects work in a similar way. For example, this line declares a String variable named word and creates a String object, as shown in Figure 9-3:

String word = "dog";
Figure 9-3. Memory diagram of a String object

Objects and arrays are usually created with the new keyword, which allocates memory for them. For convenience, you don’t have to use new to create strings:

String word = new String("dog");  // creates a string object
String word = "dog";   // implicitly creates a string object

Recall from “String Comparison” that you need to use the equals method to compare strings. The equals method traverses the String objects and tests whether they contain the same characters.

To test whether two integers or other primitive types are equal, you can simply use the == operator. But two String objects with the same characters would not be considered equal in the == sense. The == operator, when applied to string variables, tests only whether they refer to the same object.

Strings Are Immutable

If the Java library didn’t have a String class, we would have to use character arrays to store and manipulate text. Operations like concatenation (+), indexOf, and substring would be difficult and inconvenient. Fortunately, Java does have a String class that provides these and other methods.

For example, the methods toLowerCase and toUpperCase convert uppercase letters to lowercase, and vice versa. These methods are often a source of confusion, because it sounds like they modify strings. But neither these methods nor any others can change a string, because strings are immutable.

When you invoke toUpperCase on a string, you get a new String object as a result. For example:

String name = "Alan Turing";
String upperName = name.toUpperCase();

After these statements run, upperName refers to the string "ALAN TURING". But name still refers to "Alan Turing". A common mistake is to assume that toUpperCase somehow affects the original string:

String name = "Alan Turing";
name.toUpperCase();           // ignores the return value
System.out.println(name);

The previous code displays "Alan Turing", because the value of name, which refers to the original String object, never changes. If you want to change name to be uppercase, then you need to assign the return value:

String name = "Alan Turing";
name = name.toUpperCase();    // references the new string
System.out.println(name);

A similar method is replace, which finds and replaces instances of one string within another. This example replaces "Computer Science" with "CS":

String text = "Computer Science is fun!";
text = text.replace("Computer Science", "CS");

As with toUpperCase, assigning the return value (to text) is important. If you don’t assign the return value, invoking text.replace has no effect.

Strings are immutable by design, because it simplifies passing them as parameters and return values. And since the contents of a string can never change, two variables can reference the same string without one accidentally corrupting the other.

Wrapper Classes

Primitive types like int, double, and char cannot be null, and they do not provide methods. For example, you can’t invoke equals on an int:

int i = 5;
System.out.println(i.equals(5));  // compiler error

But for each primitive type, there is a corresponding wrapper class in the Java library. The wrapper class for int is named Integer, with a capital I:

Integer i = Integer.valueOf(5);
System.out.println(i.equals(5));  // displays true

Other wrapper classes include Boolean, Character, Double, and Long. They are in the java.lang package, so you can use them without importing them.

Like strings, objects from wrapper classes are immutable, and you have to use the equals method to compare them:

Integer x = Integer.valueOf(123);
Integer y = Integer.valueOf(123);
if (x == y) {                     // false
    System.out.println("x and y are the same object");
}
if (x.equals(y)) {                // true
    System.out.println("x and y have the same value");
}

Because x and y refer to different objects, this code displays only “x and y have the same value”.

Each wrapper class defines the constants MIN_VALUE and MAX_VALUE. For example, Integer.MIN_VALUE is -2147483648, and Integer.MAX_VALUE is 2147483647. Because these constants are available in wrapper classes, you don’t have to remember them, and you don’t have to write them yourself.

Wrapper classes also provide methods for converting strings to and from primitive types. For example, Integer.parseInt converts a string to an int. In this context, parse means read and translate.

String str = "12345";
int num = Integer.parseInt(str);

Other wrapper classes provide similar methods, like Double.parseDouble and Boolean.parseBoolean. They also provide toString, which returns a string representation of a value:

int num = 12345;
String str = Integer.toString(num);

The result is the String object "12345".

It’s always possible to convert a primitive value to a string, but not the other way around. For example, say we try to parse an invalid string like this:

String str = "five";
int num = Integer.parseInt(str);  // NumberFormatException

parseInt throws a NumberFormatException, because the characters in the string "five" are not digits.

Command-Line Arguments

Now that you know about strings, arrays, and wrapper classes, we can finally explain the args parameter of the main method, which we have been ignoring since Chapter 1. If you are unfamiliar with the command-line interface, please read “Command-Line Interface”.

Let’s write a program to find the maximum value in a sequence of numbers. Rather than read the numbers from System.in by using a Scanner, we’ll pass them as command-line arguments. Here is a starting point:

import java.util.Arrays;
public class Max {
    public static void main(String[] args) {
        System.out.println(Arrays.toString(args));
    }
}

You can run this program from the command line by typing this:

java Max

The output indicates that args is an empty array; that is, it has no elements:

[]

If you provide additional values on the command line, they are passed as arguments to main. For example, say you run the program like this:

java Max 10 -3 55 0 14

The output is shown here:

[10, -3, 55, 0, 14]

It’s not clear from the output, but the elements of args are strings. So args is the array {"10", "-3", "55", "0", "14"}. To find the maximum number, we have to convert the arguments to integers.

The following code uses an enhanced for loop (see “The Enhanced for Loop”) to parse the arguments and find the largest value:

int max = Integer.MIN_VALUE;
for (String arg : args) {
    int value = Integer.parseInt(arg);
    if (value > max) {
        max = value;
    }
}
System.out.println("The max is " + max);

We begin by initializing max to the smallest (most negative) number an int can represent. That way, the first value we parse will replace max. As we find larger values, they will replace max as well.

If args is empty, the result will be MIN_VALUE. We can prevent this situation from happening by checking args at the beginning of the program:

if (args.length == 0) {
    System.err.println("Usage: java Max <numbers>");
    return;
}

It’s customary for programs that require command-line arguments to display a usage message if the arguments are not valid. For example, if you run javac or java from the command line without any arguments, you will get a very long message.

Argument Validation

As we discussed in “Validating Input”, you should never assume that program input will be in the correct format. Sometimes users make mistakes, such as pressing the wrong key or misreading instructions.

Or even worse, someone might make intentional “mistakes” to see what your program will do. One way hackers break into computer systems is by entering malicious input that causes a program to fail.

Programmers can make mistakes too. It’s difficult to write bug-free software, especially when working in teams on large projects.

For all of these reasons, it’s good practice to validate arguments passed to methods, including the main method. In the previous section, we did this by ensuring that args.length was not 0.

As a further example, consider a method that checks whether the first word of a sentence is capitalized. We can write this method using the Character wrapper class:

public static boolean isCapitalized(String str) {
    return Character.isUpperCase(str.charAt(0));
}

The expression str.charAt(0) makes two assumptions: the string object referenced by str exists, and it has at least one character. What if these assumptions don’t hold at run-time?

We can prevent these exceptions by validating str at the start of the method. If it’s invalid, we return before executing the rest of the method:

public static boolean isCapitalized(String str) {
    if (str == null || str.isEmpty()) {
        return false;
    }
    return Character.isUpperCase(str.charAt(0));
}

Notice that null and empty are different concepts, as shown in Figure 9-5. The variable str1 is null, meaning that it doesn’t reference an object. The variable str2 refers to the empty string, an object that exists.

Figure 9-5. Memory diagram of null and empty string

Beginners sometimes make the mistake of checking for empty first. Doing so causes a NullPointerException, because you can’t invoke methods on variables that are null:

if (str.isEmpty() || str == null) {    // wrong!

Checking for null first prevents the NullPointerException. If str is null, the || operator will short-circuit (see “Logical Operators”) and evaluate to true immediately. As a result, str.isEmpty() will not be called.

BigInteger Arithmetic

It might not be clear at this point why you would ever need an integer object when you can just use an int or long. One advantage is the variety of methods that Integer and Long provide. But there is another reason: when you need very large integers that exceed Long.MAX_VALUE.

BigInteger is a Java class that can represent arbitrarily large integers. There is no upper bound except the limitations of memory size and processing speed. Take a minute to read the documentation, which you can find by doing a web search for “Java BigInteger”.

To use BigIntegers, you have to import java.math.BigInteger at the beginning of your program. There are several ways to create a BigInteger, but the simplest uses valueOf. The following code converts a long to a BigInteger:

long x = 17;
BigInteger big = BigInteger.valueOf(x);

You can also create BigIntegers from strings. For example, here is a 20-digit integer that is too big to store using a long:

String s = "12345678901234567890";
BigInteger bigger = new BigInteger(s);

Notice the difference in the previous two examples: you use valueOf to convert integers, and new BigInteger to convert strings.

Since BigIntegers are not primitive types, the usual math operators don’t work. Instead, we have to use methods like add. To add two BigIntegers, we invoke add on one and pass the other as an argument:

BigInteger a = BigInteger.valueOf(17);
BigInteger b = BigInteger.valueOf(1700000000);
BigInteger c = a.add(b);

Like strings, BigInteger objects are immutable. Methods like add, multiply, and pow all return new BigIntegers, rather than modify an existing one.

Internally, a BigInteger is implemented using an array of ints, similar to the way a string is implemented using an array of chars. Each int in the array stores a portion of the BigInteger. The methods of BigInteger traverse this array to perform addition, multiplication, etc.

For very long floating-point values, take a look at java.math.BigDecimal. Interestingly, BigDecimal objects represent floating-point numbers internally by using a BigInteger!

Incremental Design

One challenge of programming, especially for beginners, is figuring out how to divide a program into methods. In this section, we present a design process that allows you to divide a program into methods as you go along. The process is called encapsulation and generalization. The essential steps are as follows:

  1. Write a few lines of code in main or another method, and test them.

  2. When they are working, wrap them in a new method and test again.

  3. If it’s appropriate, replace literal values with variables and parameters.

To demonstrate this process, we’ll develop methods that display multiplication tables. We begin by writing and testing a few lines of code. Here is a loop that displays the multiples of two, all on one line:

for (int i = 1; i <= 6; i++) {
    System.out.printf("%4d", 2 * i);
}
System.out.println();

Each time through the loop, we display the value of 2 * i, padded with spaces so it’s four characters wide. Since we use System.out.printf, the output appears on a single line.

After the loop, we call println to print a newline character. Remember that in some environments, none of the output is displayed until the line is complete. The output of the code so far is shown here:

   2   4   6   8  10  12

The next step is to encapsulate the code; that is, we wrap the code in a method:

public static void printRow() {
    for (int i = 1; i <= 6; i++) {
        System.out.printf("%4d", 2 * i);
    }
    System.out.println();
}

Finally, we generalize the method to print multiples of other numbers by replacing the constant value 2 with a parameter n. This step is called generalization, because it makes the method more general (less specific):

public static void printRow(int n) {
    for (int i = 1; i <= 6; i++) {
        System.out.printf("%4d", n * i);  // generalized n
    }
    System.out.println();
}

Invoking this method with the argument 2 yields the same output as before. With the argument 3, the output is as follows:

   3   6   9  12  15  18

By now, you can probably guess how we are going to display a multiplication table: we’ll invoke printRow repeatedly with different arguments. In fact, we’ll use another loop to iterate through the rows:

for (int i = 1; i <= 6; i++) {
    printRow(i);
}

And the output looks like this:

   1   2   3   4   5   6
   2   4   6   8  10  12
   3   6   9  12  15  18
   4   8  12  16  20  24
   5  10  15  20  25  30
   6  12  18  24  30  36

More Generalization

The previous result is similar to the nested loops approach in “Nested Loops”. However, the inner loop is now encapsulated in the printRow method. We can encapsulate the outer loop in a method too:

public static void printTable() {
    for (int i = 1; i <= 6; i++) {
        printRow(i);
    }
}

The initial version of printTable always displays six rows. We can generalize it by replacing the literal 6 with a parameter:

public static void printTable(int rows) {
    for (int i = 1; i <= rows; i++) {     // generalized rows
        printRow(i);
    }
}

Here is the output of printTable(7):

   1   2   3   4   5   6
   2   4   6   8  10  12
   3   6   9  12  15  18
   4   8  12  16  20  24
   5  10  15  20  25  30
   6  12  18  24  30  36
   7  14  21  28  35  42

That’s better, but it always displays the same number of columns. We can generalize more by adding a parameter to printRow:

public static void printRow(int n, int cols) {
    for (int i = 1; i <= cols; i++) {     // generalized cols
        System.out.printf("%4d", n * i);
    }
    System.out.println();
}

Now printRow takes two parameters: n is the value whose multiples should be displayed, and cols is the number of columns. Since we added a parameter to printRow, we also have to change the line in printTable where it is invoked:

public static void printTable(int rows) {
    for (int i = 1; i <= rows; i++) {
        printRow(i, rows);
    }
}

When this line executes, it evaluates rows and passes the value, which is 7 in this example, as an argument. In printRow, this value is assigned to cols. As a result, the number of columns equals the number of rows, so we get a square 7 × 7 table, instead of the previous 7 × 6 table.

When you generalize a method appropriately, you often find that it has capabilities you did not plan. For example, you might notice that the multiplication table is symmetric. Since ab=ba, all the entries in the table appear twice. You could save ink by printing half of the table, and you would have to change only one line of printTable:

printRow(i, i);  // using i for both n and cols

This means the length of each row is the same as its row number. The result is a triangular multiplication table:

   1
   2   4
   3   6   9
   4   8  12  16
   5  10  15  20  25
   6  12  18  24  30  36
   7  14  21  28  35  42  49

Generalization makes code more versatile, more likely to be reused, and sometimes easier to write.

Exercises

The code for this chapter is in the ch09 directory of ThinkJavaCode2. See “Using the Code Examples” for instructions on downloading the repository. Before you start the exercises, we recommend that you compile and run the examples.

Exercise 9-4.

One way to calculate ex is to use the following infinite series expansion. The ith term in the series is xi/i!.

  1. Write a method called myexp that takes x and n as parameters and estimates ex by adding the first n terms of this series. You can use the factorial method from “Value-Returning Methods” or your iterative version from the previous exercise.

  2. You can make this method more efficient by observing that the numerator of each term is the same as its predecessor multiplied by x, and the denominator is the same as its predecessor multiplied by i.

    Use this observation to eliminate the use of Math.pow and factorial, and check that you get the same result.

  3. Write a method called check that takes a parameter, x, and displays x, myexp(x), and Math.exp(x). The output should look something like this:

    1.0     2.708333333333333     2.718281828459045

    You can use the escape sequence "\t" to put a tab character between columns of a table.

  4. Vary the number of terms in the series (the second argument that check sends to myexp) and see the effect on the accuracy of the result. Adjust this value until the estimated value agrees with the correct answer when x is 1.

  5. Write a loop in main that invokes check with the values 0.1, 1.0, 10.0, and 100.0. How does the accuracy of the result vary as x varies? Compare the number of digits of agreement rather than the difference between the actual and estimated values.

  6. Add a loop in main that checks myexp with the values -0.1, -1.0, -10.0, and -100.0. Comment on the accuracy.

Exercise 9-5.

The goal of this exercise is to practice encapsulation and generalization using some of the examples in previous chapters.

  1. Starting with the code in “Traversing Arrays”, write a method called powArray that takes a double array, a, and returns a new array that contains the elements of a squared. Generalize it to take a second argument and raise the elements of a to the given power.

  2. Starting with the code in “The Enhanced for Loop”, write a method called histogram that takes an int array of scores from 0 to (but not including) 100, and returns a histogram of 100 counters. Generalize it to take the number of counters as an argument.