Chapter 7: String Manipulations

In the early 1960s, a message typed on one company’s computer could arrive on another company’s machine as nonsense. The trouble was not the wires. Every manufacturer had invented its own private table deciding which number stood for the letter A, which for a comma, which for a blank space. Two machines could pass numbers to each other perfectly and still disagree completely about what those numbers meant.

The answer was not a cleverer machine. It was an agreement. In 1963 an American standards committee published a single shared table, the American Standard Code for Information Interchange (ASCII), fixing one number for each letter, digit, punctuation mark and control character. Once everyone accepted the same table, text became something that could be stored, sent and compared by machines that had never been designed to work together.

That agreement is still underneath every program you write. When your Java program asks whether one word comes before another in a dictionary, it is not reading the words. It is comparing numbers, in an order that people fixed on paper long before your machine existed.

This chapter is about working with those characters and words in Java — testing them, joining them, splitting them, searching them and rearranging them.

Diagram

Fig. 7.0-1 – flow: the word “CAT” typed on Machine A → encoded as the numbers 67 65 84 using Machine A’s private code table → sent over a link → decoded by Machine B using a different private code table → printed as meaningless symbols; below this, the same journey repeated with one shared code table on both sides, printing “CAT” correctly; label the two paths “different tables” and “one agreed table”

NoteCuriosity Corner

Q1. Java says the words “Anil” and “anil” are not equal. Why does a capital letter count as a different character from the same small letter? Q2. If a character is really stored as a number, what happens when a program adds 1 to the character ‘A’? Q3. Java already has the type int for whole numbers. Why does it also provide a class called Integer? Q4. A program builds a long sentence by joining one word at a time inside a loop. It works for ten words but crawls for ten thousand. What is going wrong, and which class repairs it? Q5. How can a program break a sentence into separate words without examining the sentence one character at a time?

Each of these questions is answered somewhere in this chapter. Read on, and see whether your first guess survives.

7.1 Primitive Values and Wrapper Classes

Before a program can work with words, it has to work with the smaller things words are made of — single characters and single numbers. Java gives you both in two very different forms, and knowing which form you are holding decides what you are allowed to do with it.

You already know the eight primitive types from Class XI: byte, int, short, long, float, double, boolean and char. A primitive type is a type built into the language itself; a variable of such a type holds one plain value and nothing else.

That “nothing else” is the point of this section.

7.1.1 Why a Primitive Value Is Sometimes Not Enough

Think about a school fee counter. The clerk types 450 and the program has to multiply it by the number of months. What the program actually receives from the keyboard is not the number four hundred and fifty. It is the text "450" — three characters sitting side by side.

You cannot multiply text. Somewhere, something has to look at those three characters and work out the whole number they stand for. Where should that piece of skill live?

It cannot live in the int type. An int variable is a labelled box with a value in it; it carries no instructions and can be asked no questions. Ask it what the largest value an int can hold is, and there is nobody to ask.

In Java, skill lives in classes. So for each primitive type Java also provides a class whose objects hold one value of that type — and, because it is a class, it can also carry the methods and constants that go with that type. These are the wrapper classes.

NoteDefinition

A wrapper class is a class whose object holds a single value of one primitive type, and which supplies the methods and constants associated with that type.

The name is a picture of what happens: the plain value is wrapped inside an object, the way a sweet is wrapped in paper. The value is unchanged; what it gains is a covering that other parts of Java know how to handle.

Look at the difference in memory, shown in Fig. 7.1-1. A primitive variable is a single box. A wrapper object is a box with a value inside it, reached through a variable that only points to it.

Diagram

Fig. 7.1-1 – two labelled memory pictures side by side; on the left, a single box labelled int n containing the value 45; on the right, a small box labelled Integer obj holding an arrow that points to a larger box drawn as an object, with the value 45 inside it and the method names intValue and toString listed below the value; caption the two pictures “a value” and “an object holding a value”

Two things follow from this picture, and both matter later in the chapter. First, the wrapper object can be asked to do work. Second, wherever Java insists on an object, a wrapped value will be accepted and a bare primitive value will not.

7.1.2 The Eight Wrapper Classes

There is exactly one wrapper class for each primitive type — eight types, eight classes. Table 7.1 lists them in the same order as the primitive types you learnt in Class XI.

Table 7.1 – the eight primitive types and their wrapper classes, with the kind of value each holds

Primitive type Wrapper class Value held
byte Byte a very small whole number
int Integer a whole number
short Short a small whole number
long Long a large whole number
float Float a number with a fractional part
double Double a more precise number with a fractional part
boolean Boolean true or false
char Character a single character

Read the two middle columns together and one rule stands out. Every primitive type name is written entirely in small letters, because it is a keyword of the language. Every wrapper class name begins with a capital letter, because it is a class. int and Integer are not two spellings of one thing; they are a keyword and a class.

Notice also that two of the names are not simply the primitive name with a capital letter. The wrapper for int is Integer, and the wrapper for char is Character.

Q. Would Char ch; compile?

A. No. There is no class named Char in Java, so the compiler reports that it cannot find the type. The class you want is Character.

All eight classes belong to the package java.lang, which every Java program imports automatically. You never write an import statement to use them.

Six of the eight — Byte, Short, Integer, Long, Float and Double — hold numbers, and all six are grouped under a common class named Number. Character and Boolean hold values that are not numbers, and they stand outside that grouping.

Because a wrapper class is a class, it can carry constants as well as methods. Integer.MAX_VALUE and Integer.MIN_VALUE name the largest and smallest values an int can hold. That is knowledge about the type int — and it had to be kept somewhere, because the type itself cannot hold it.

Table 7.2 – a primitive value and a wrapper object

Feature Primitive value (int n) Wrapper object (Integer obj)
What the variable holds the value itself a reference to an object that holds the value
Methods available none methods such as intValue() and toString()
Constants available none constants such as Integer.MAX_VALUE
Accepted where an object is required no yes
Memory used just the value the object, plus the reference to it
Speed of ordinary arithmetic faster slower, since the value must be taken out first

The last two rows are the honest limitation. A wrapper object costs more memory and more time than a plain value. So the working rule for the rest of this book is simple: use the primitive type for ordinary calculation, and reach for the wrapper class when you need what only a class can give.

Q3. Java already has the type int for whole numbers. Why does it also provide a class called Integer?

A. Because int is a type, not a class, and a plain value cannot carry methods or constants. Everything Java knows about whole numbers as a type — how to build one from the text a user typed, how to turn one back into text, what the largest possible value is — has to live somewhere, and in Java that somewhere must be a class. Integer is that class. It also lets a whole number be passed to any part of Java that insists on receiving an object.

7.1.3 Creating a Wrapper Object

A wrapper object is created like any other object, with new and a constructor that takes the value to be wrapped.

Integer count = new Integer(45);
Character grade = new Character('A');
Double price = new Double(12.5);

To get the plain value back out, you call a method on the object. Each wrapper class provides one named after the primitive type it holds.

int c = count.intValue();
char g = grade.charValue();
double p = price.doubleValue();

The next program does nothing clever. It only makes visible that the value going in and the value coming out are the same, and that the object can answer a question the primitive could not.

Storing a count as an object and reading it back Problem: A stationery shop records the number of notebooks sold in a day. Store the count as a primitive value, wrap it in an Integer object, take the value out again, and also display the largest value an int can hold. The count is fixed inside the program.

class NotebookCount
{
    public static void main(String args[])
    {
        int sold = 37;
        Integer soldObject = new Integer(sold);
        int backAgain = soldObject.intValue();
        System.out.println("Primitive value  : " + sold);
        System.out.println("Object value     : " + soldObject);
        System.out.println("Unwrapped value  : " + backAgain);
        System.out.println("Largest int      : " + Integer.MAX_VALUE);
    }
}

Sample output:

Primitive value  : 37
Object value     : 37
Unwrapped value  : 37
Largest int      : 2147483647

Variable description:

[Table:] Table 7.3 – variables used in NotebookCount [/Table] | Variable | Type | Purpose | |—|—|—| | sold | int | the plain count of notebooks sold | | soldObject | Integer | the same count held as an object | | backAgain | int | the value taken back out of the object |

Notes: The third line of output is the interesting one — wrapping and unwrapping returns the original value untouched, so nothing is lost in the round trip. The second line works because printing an object makes Java ask that object for its text form, and Integer supplies one. The last line comes not from any object but from the class itself, which is why it is written as Integer.MAX_VALUE and not soldObject.MAX_VALUE. This book assumes Java 5.0 or later, as the syllabus specifies; from Java 9 onwards a compiler using the newest libraries will warn that the new Integer(...) constructor is discouraged, and the shorter form shown in the next subsection is preferred.

7.1.4 Autoboxing and Unboxing

Writing new Integer(...) every time a value has to become an object is tiresome, and taking it out again with intValue() is no better. From Java 5.0 onwards the compiler does both conversions for you when it can see that they are needed.

Wrapping happens automatically on assignment to a wrapper variable:

Integer p = 32;
Character ch = 'A';

Unwrapping happens automatically on assignment to a primitive variable:

Integer n = 45;
int t = n;

These two automatic conversions have names.

NoteDefinition

Autoboxing is the automatic conversion, performed by the compiler, of a primitive value into an object of its corresponding wrapper class.

NoteDefinition

Unboxing is the automatic conversion, performed by the compiler, of a wrapper object into the primitive value it holds.

Nothing new happens inside the machine. The compiler simply writes the longer form on your behalf, as Fig. 7.1-2 shows.

Diagram

Fig. 7.1-2 – two horizontal arrows between two labelled boxes; the upper arrow runs from a box labelled “primitive value int 32” to a box labelled “wrapper object Integer holding 32” and is labelled “autoboxing”; the lower arrow runs back from the object box to the primitive box and is labelled “unboxing”; alongside each arrow print the statement that triggers it, Integer p = 32; above and int t = p; below

Because unboxing is automatic, a wrapped value can even be used directly in arithmetic:

Integer marks = 78;
int total = marks + 12;

Here the compiler unboxes marks to 78, adds 12, and stores 90 in total. The convenience is real, but it hides a genuine trap.

comparing two wrapper objects with == What students write:

Integer a = 500;
Integer b = 500;
System.out.println(a == b);

What happens: this prints false, even though both objects hold 500. The operator == compares two object variables by asking whether they refer to the same object, not whether the values inside are equal — and autoboxing created two separate objects here. Worse, the same code with the value 100 in place of 500 prints true, because Java keeps a ready-made object for every whole number from −128 to 127 and hands out the same one twice. Code that behaves one way for small numbers and another way for large ones is code you cannot trust.

The fix:

int a = 500;
int b = 500;
System.out.println(a == b);

Compare plain values with ==, and objects with a method such as equals(). The operator asks about identity; the method asks about content. You will meet exactly this distinction again in section 7.4.5, where it decides whether two words count as the same word.

Rewrite NotebookCount so that the count is a double price such as 24.75, held in a Double object, and use autoboxing and unboxing instead of new and doubleValue(). Before you run it, write down the four lines you expect, then check whether the last line still prints a whole number and explain why it changes.

7.2 The Character Class

Almost every interesting question about a word is really a question about its characters. Is this token a number or a name? Where does one word end and the next begin? Should this letter be printed in capitals?

Java keeps the tools for answering such questions in the wrapper class Character. This section is about those tools, and about the numeric codes that make them possible.

7.2.1 Characters and Their Numeric Codes

A char variable holds one character, written between single quotes:

char grade = 'A';
char digit = '7';
char blank = ' ';

Inside the machine, though, there is no such thing as a letter. There are only numbers. So each character is stored as the number that the agreed code table gives it — 65 for 'A', 55 for '7', 32 for the blank space.

Java uses Unicode, a code table designed to cover the writing systems of the world. Its first 128 codes are exactly the codes of ASCII, which is why the values in Table 7.4 are the ones you will meet in every Class XII program.

Table 7.4 – the code ranges used most often, with the first and last code in each group

Group of characters First Last
Tab '\t' 9 9
Newline '\n' 10 10
Space ' ' 32 32
Digits '0' to '9' 48 57
Capital letters 'A' to 'Z' 65 90
Small letters 'a' to 'z' 97 122

Three facts in this table do most of the work in this chapter, and Fig. 7.2-1 shows why.

Diagram

Fig. 7.2-1 – a horizontal number line from 0 to 127 with four shaded bands marked on it: a narrow band at 32 labelled “space”, a band from 48 to 57 labelled “digits 0–9”, a band from 65 to 90 labelled “capitals A–Z”, and a band from 97 to 122 labelled “small a–z”; draw a double-headed arrow between 65 and 97 labelled “gap of 32”, and mark the codes 9 and 10 near the left end as “tab” and “newline”

First, each group is unbroken and in order. 'B' follows 'A', 'C' follows 'B', and the digit characters run from '0' up to '9' without a gap. That is what makes it possible to test whether a character is a letter by comparing codes.

Second, the same letter in the two cases has two different codes, and the difference is always 32. 'A' is 65 and 'a' is 97; 'Z' is 90 and 'z' is 122.

Third — and students trip on this every year — the digit character '7' is not the number 7. It carries the code 55. A character typed at the keyboard is a symbol, not a quantity, and turning one into the other takes work you will do yourself in section 7.2.4.

Q1. Java says the words “Anil” and “anil” are not equal. Why does a capital letter count as a different character from the same small letter?

A. Because the machine never compares letters — it compares codes, and the two cases were given different codes. 'A' is 65 while 'a' is 97, so a test for equality fails at the very first character. The same fact settles the order as well: 65 is smaller than 97, so in Java’s ordering every capital letter comes before every small letter, and “Anil” is treated as the smaller of the two words.

7.2.2 Testing a Character

You could write these tests yourself by comparing codes, and later in this chapter you will see why that is sometimes worth doing. But the Character class already carries them, correctly written and clearly named.

Each of these methods is called on the class itself rather than on an object, so the class name is written in front of it — Character.isDigit(ch). Each takes one char and returns a boolean.

Table 7.5 – methods of the Character class that test a character

Method Returns true when the character is Example that returns true
isLetter(char) a letter, capital or small 'k'
isDigit(char) one of the digits '0' to '9' '4'
isLetterOrDigit(char) a letter or a digit 'P' or '9'
isUpperCase(char) a capital letter 'M'
isLowerCase(char) a small letter 'm'
isWhitespace(char) a space, a tab or a newline ' '

The last one deserves its own word. Whitespace is any character that occupies space on the line without printing a mark of its own: the blank space, the tab and the newline. It is the whitespace characters that separate one word from the next, so any program that works with words has to be able to recognise them.

Now a small problem to put the tests to work. A vehicle registration code is a mixture of letters, digits and a blank space, and a program checking such a code needs to know how many of each it has been given.

Counting letters, digits and blanks in a code Problem: A registration code is stored in the program as a sequence of characters. Count how many of them are letters, how many are digits and how many are blank spaces, and display the three counts.

class CodeCheck
{
    public static void main(String args[])
    {
        char code[] = {'R', 'J', '1', '4', ' ', 'C', 'A', '2', '8'};
        int letters = 0;
        int digits = 0;
        int blanks = 0;
        for (int i = 0; i < code.length; i++)
        {
            if (Character.isLetter(code[i]))
            {
                letters = letters + 1;
            }
            else if (Character.isDigit(code[i]))
            {
                digits = digits + 1;
            }
            else if (Character.isWhitespace(code[i]))
            {
                blanks = blanks + 1;
            }
        }
        System.out.println("Letters : " + letters);
        System.out.println("Digits  : " + digits);
        System.out.println("Blanks  : " + blanks);
    }
}

Sample output:

Letters : 4
Digits  : 4
Blanks  : 1

Variable description:

[Table:] Table 7.6 – variables used in CodeCheck [/Table] | Variable | Type | Purpose | |—|—|—| | code | char[] | the characters of the registration code | | i | int | position of the character being examined | | letters | int | running count of letters found so far | | digits | int | running count of digits found so far | | blanks | int | running count of blank spaces found so far |

Notes: The three tests are joined by else if rather than written as three separate if statements, because a character can belong to only one of these groups. Once isLetter has said yes, there is no reason to ask the other two. The three counters all start at 0, which is the correct answer for a code with no characters at all — a small point, but it is the reason the program is right for every input and not merely for this one.

CodeCheck on the code R J 1 4 (space) C A 2 8

i code[i] test that succeeds letters digits blanks
0 'R' isLetter 1 0 0
1 'J' isLetter 2 0 0
2 '1' isDigit 2 1 0
3 '4' isDigit 2 2 0
4 ' ' isWhitespace 2 2 1
5 'C' isLetter 3 2 1
6 'A' isLetter 4 2 1
7 '2' isDigit 4 3 1
8 '8' isDigit 4 4 1

Result: the loop ends with i equal to 9, and the program prints 4, 4 and 1. The trace shows that every character increases exactly one counter, so the three counts must always add up to the number of characters examined — a useful way to check such a program on any input.

7.2.3 Converting a Character

Two more methods of Character change a character rather than test it, and one turns it into text.

Table 7.7 – methods of the Character class that convert a character

Method Result
toUpperCase(char) the capital form of the character
toLowerCase(char) the small form of the character
toString(char) the character as a String of length one

The first two are gentler than their names suggest. They convert only when there is something to convert; a character that is already in the wanted case, or that is not a letter at all, is returned unchanged.

System.out.println(Character.toUpperCase('a'));
System.out.println(Character.toUpperCase('M'));
System.out.println(Character.toUpperCase('7'));

prints

A
M
7

The third method matters because a character and a one-character piece of text are not the same thing in Java. 'A' is a char, written between single quotes; "A" is a String, written between double quotes. Character.toString('A') builds the second from the first, which is what you need whenever a character must be joined to a piece of text.

passing a code number where a character is expected What students write:

System.out.println(Character.toUpperCase(97));

What happens: it compiles, and prints 65 instead of A. The value 97 is an int, not a char, so a different version of the method runs — one that takes a code number and returns a code number. The conversion was done correctly; it is the printing that surprises you, because printing an int shows the number.

The fix:

System.out.println(Character.toUpperCase('a'));

Pass the character itself, in single quotes, and the method returns a char, which prints as the letter A. If you genuinely hold a code number, convert it to a character first, as the next subsection shows.

7.2.4 Arithmetic on Characters

Since a character is stored as a number, Java lets you do arithmetic with it. Wherever a char is used in a calculation, its code is used in place of it.

System.out.println('A' + 1);

This prints 66, not B. The addition produced an int, and an int prints as a number. To see the character with code 66, you must tell Java to treat that number as a character, using a type cast — a conversion you write yourself, in brackets, in front of the value.

System.out.println((char)('A' + 1));

Now the output is B.

Q2. If a character is really stored as a number, what happens when a program adds 1 to the character ‘A’?

A. Java replaces 'A' by its code, 65, and works out 66 as an ordinary int. Nothing has become a letter yet — the result is a number, and printing it shows 66. Write (char)('A' + 1) and you ask Java to read 66 back as a character, which gives 'B'. This works only because the capital letters were given consecutive codes; adding 1 to 'Z' gives 91, which is not a letter at all.

Two everyday uses follow from this, and both appear again later in the chapter.

To find the numeric value of a digit character, subtract the code of '0':

char c = '7';
int value = c - '0';

'7' is 55 and '0' is 48, so value becomes 7 — the quantity, at last, rather than the symbol.

To change the case of a letter without calling a method, use the gap of 32:

char small = 'd';
char capital = (char)(small - 32);

forgetting the cast when the character is in a variable What students write:

char ch = 'A';
char next = ch + 1;

What happens: the program does not compile. The compiler reports a possible lossy conversion from int to char, because ch + 1 is an int and Java will not quietly squeeze an int into a char variable. Curiously, char next = 'A' + 1; does compile, because there the value can be worked out at compile time and checked to fit — which is exactly why the error looks so unpredictable.

The fix:

char ch = 'A';
char next = (char)(ch + 1);

Write the cast yourself. You are telling the compiler that you know the value fits, and taking responsibility for it.

The gap of 32 between the cases was not an accident. In binary, 32 is a single bit, so 'A' and 'a' differ in exactly one bit position. Early hardware could switch a letter’s case by flipping that one bit, with no arithmetic at all.

Write a program that prints the capital letters 'A' to 'Z' on one line, using a single char variable and a loop that adds 1 each time. Then change it to print each letter together with its code. Predict what your loop does if you let it run one step too far, and check whether you were right.

7.3 The String Class

A single character is rarely what a program is really about. Names, addresses, sentences, registration codes, menu choices — all of them are runs of characters that have to be kept together and handled as one thing.

Java keeps such a run in an object of the class String, and gives that class more support from the language than any other class in the library.

7.3.1 What a String Object Holds

A String object holds a fixed sequence of characters, along with the count of how many there are. Text written between double quotes is a string literal, and each literal is itself a String object waiting in the program.

String city = "Jaipur";
String empty = "";

The characters of a string are numbered from 0, exactly as the cells of an array are. Fig. 7.3-1 shows the numbering for a nine-character string.

Diagram

Fig. 7.3-1 – the string “RAJASTHAN” drawn as a row of nine equal boxes, one character per box, reading R A J A S T H A N; below each box print its index 0 to 8; above the row write the label “length is 9”; draw a small arrow to the last box labelled “last index is length − 1”

That picture carries the single most useful fact about strings. A string of length 9 has indexes running from 0 to 8, so the last index is always one less than the length. Almost every mistake a student makes with strings is a mistake about that one.

NoteDefinition

A String object is an object that holds a fixed sequence of characters, numbered from 0 up to one less than the length of the sequence.

Two warnings about what a String is not.

It is not a primitive type. String is a class, so a String variable holds a reference to an object, not the characters themselves. The language does give it two conveniences no other class receives — you may create one by writing a literal, and you may join two with the + operator — but those are conveniences, not a change of status.

It is also not an array. Even though a string keeps its characters in order and numbers them from 0, you cannot write city[0]. The characters are reached by calling a method, which you will meet in section 7.4.1.

Table 7.8 – a char value and a String object

Feature char String
What it holds exactly one character any number of characters, including none
How it is written between single quotes, 'A' between double quotes, "A"
Kind of type primitive type class
Methods available none on the value itself many, such as length()
Result of joining with + codes are added, unless text is involved characters are joined end to end

The third row is worth reading twice. 'A' and "A" look almost the same on the page and are entirely different things to the compiler: one is a value of a primitive type, the other is an object.

Q. What does System.out.println('A' + 'B'); print?

A. 131. Both are char values, so Java adds their codes, 65 and 66. Change either one to double quotes and the answer becomes AB, because joining to text is a different operation altogether.

7.3.2 Creating a String Object

There are two ways to bring a String object into existence, and the difference between them is worth understanding once, properly.

The first is to assign a literal:

String subject = "Computer Science";

The second is to use new with a constructor of the class:

String subject = new String("Computer Science");

Both give you a string with the same sixteen characters, and every method in this chapter behaves identically on the two. What differs is how many objects exist.

Java keeps a single store of the string literals appearing in a program. Write the same literal twice and both variables are pointed at the one stored object. Write new, and Java is being ordered to build a fresh object, so a second one is made even though its contents are identical.

String a = "cat";
String b = "cat";
String c = new String("cat");

Here a and b refer to the same object, while c refers to a different object holding the same three characters, as Fig. 7.3-2 shows.

Diagram

Fig. 7.3-2 – three small labelled boxes on the left for the variables a, b and c; arrows from a and b meeting at a single object box on the right containing the characters c a t and labelled “shared literal”; a separate arrow from c to a second object box, also containing c a t, labelled “built by new”

This is not a curiosity. It is the reason that comparing two strings with == sometimes appears to work and sometimes does not, which is settled properly in section 7.4.5. The safe habit starts here: use a literal unless you have a reason not to, and never test two strings for equality with ==.

A third constructor is occasionally useful. Given an array of characters, it builds the string those characters spell:

char letters[] = {'S', 'I', 'T', 'A'};
String name = new String(letters);

name now holds "SITA". You will find a use for this in section 7.5, when characters have been rearranged in an array and must be turned back into a word.

7.3.3 Why a String Cannot Be Changed

Here is the property that governs everything else in this chapter. Once a String object exists, the characters inside it never change. A string is immutable.

NoteDefinition

An object is immutable if its contents cannot be altered after it has been created; every operation that appears to change it in fact produces a new object, leaving the original untouched.

This sounds harmless until you see what it does to a program.

Showing that a string is never changed Problem: Store a name in small letters, ask for its capital form, and then display the original name, the capital form and the length of the original — so that it is visible which of them changed. The name is fixed inside the program.

class NameCase
{
    public static void main(String args[])
    {
        String name = "meera";
        String shout = name.toUpperCase();
        System.out.println("Original : " + name);
        System.out.println("Result   : " + shout);
        System.out.println("Length   : " + name.length());
    }
}

Sample output:

Original : meera
Result   : MEERA
Length   : 5

Variable description:

[Table:] Table 7.9 – variables used in NameCase [/Table] | Variable | Type | Purpose | |—|—|—| | name | String | the original name, in small letters | | shout | String | the capital form returned by toUpperCase() |

Notes: The two methods used here are described fully in section 7.4; what matters now is the first line of output. After toUpperCase() has run, name still holds "meera". The method did not change the string it was called on — it could not — so it built a second string and handed it back. Had the result not been caught in shout, it would simply have been thrown away.

expecting a string method to change the string What students write:

String title = "annual report";
title.toUpperCase();
System.out.println(title);

What happens: it compiles, it runs, and it prints annual report. Nothing appears to have gone wrong, which is what makes this mistake so hard to find. The capital form was built and then discarded, because no variable was told to hold it.

The fix:

String title = "annual report";
title = title.toUpperCase();
System.out.println(title);

Catch the returned string. Read every string method as a question that is answered, never as an order that is obeyed.

Fig. 7.3-3 shows what actually happened in the corrected version.

Diagram

Fig. 7.3-3 – a variable box labelled title with a crossed-out arrow to an object containing “annual report” marked “unchanged, now unused”, and a new solid arrow to a second object containing “ANNUAL REPORT” marked “newly built”; label the whole figure “the variable moved; the object did not change”

Why would a language be designed this way? Because a shared object that nobody can alter is a safe object. Since the characters of a literal can never change, Java is free to let several variables share one copy of it, as you saw in section 7.3.2, without any risk that one part of a program will silently damage the text another part is using.

The price is paid whenever a string is built up a piece at a time.

building a string by repeated joining Let \(n\) be the number of characters in the finished string. Each join copies every character built so far into a brand-new object, so the copies made are roughly \(1 + 2 + 3 + \dots + n\). The dominant term is \(n^2/2\), giving \(O(n^2)\) work to assemble a string of \(n\) characters one character at a time.

For a name or a sentence this cost is invisible. For a report of a hundred thousand characters it is the difference between a program that finishes and one that seems to hang. Section 7.7 introduces the class Java provides for exactly that situation.

Write a two-line program that stores "jaipur" in a variable, calls toUpperCase() on it without assigning the result, and prints the variable. Then add one word to one line so that the capital form is printed instead. Note down which word you added — that is the whole of immutability in a single edit.

7.4 Operations on Strings

The String class supplies a large set of methods, and a student meeting them as a list has no way of telling which matter. So this section groups them by the question they answer: how long is it, what is inside it, how do I join it, how do I compare it, where is this word, and how do I put something else in its place.

Every one of them obeys the rule established in section 7.3.3. None of them changes the string it is called on. Each returns an answer — a number, a boolean, or a brand-new string — and it is your job to catch it.

7.4.1 Length and Access to Individual Characters

Two methods do almost all the ordinary work.

length() returns the number of characters in the string, as an int. charAt(i) returns the character standing at index i, as a char.

String word = "Jaipur";
System.out.println(word.length());
System.out.println(word.charAt(0));
System.out.println(word.charAt(word.length() - 1));

prints

6
J
r

The third line is the standard way to reach the last character, and it is worth memorising as a pattern rather than as a fact. There is no index 6 in a string of length 6, because the indexes are 0 to 5.

Q. Why is it length() for a string but length for an array?

A. An array is not a class you can add methods to, so its size is exposed directly as a field, written without brackets. String is an ordinary class, and it reports its count through a method. The inconsistency is a wart in the language, not a rule you can reason out — you simply have to remember it.

Together these two methods let a program walk through a string one character at a time, which is the foundation of nearly every string algorithm in section 7.5.

reaching past the last character What students write:

String word = "Jaipur";
for (int i = 0; i <= word.length(); i++)
{
    System.out.println(word.charAt(i));
}

What happens: the six characters print correctly, and then the program stops with StringIndexOutOfBoundsException. On the seventh turn i is 6, and index 6 does not exist. The program is not wrong by much — it is wrong by exactly one.

The fix:

for (int i = 0; i < word.length(); i++)

Use <, not <=. The count of characters and the last index are different numbers, and a loop that runs while i is less than the length visits every index exactly once.

7.4.2 Extracting a Substring

A substring is any run of consecutive characters taken from a string. The method substring() comes in two forms.

With one argument, s.substring(begin) returns everything from index begin to the end of the string.

With two arguments, s.substring(begin, end) returns the characters from index begin up to but not including index end.

String s = "Computer";
System.out.println(s.substring(3));
System.out.println(s.substring(3, 6));

prints

puter
put

The second rule is the one that catches everybody, so look at Fig. 7.4-1 rather than trying to remember words.

Diagram

Fig. 7.4-1 – the string “Computer” drawn as eight boxes with indexes 0 to 7 written below them; two vertical cut marks drawn between boxes, one immediately before the box at index 3 labelled “begin = 3, taken”, the other immediately before the box at index 6 labelled “end = 6, not taken”; shade the three boxes at indexes 3, 4 and 5 and label the shaded run “the result, put”

Read the two arguments as cut positions rather than as characters, and the rule stops feeling arbitrary. The cuts fall before the index you name. That also explains the length of the result, which is always end − begin.

Q. What is "Computer".substring(4, 4)?

A. The empty string, "". Both cuts fall in the same place, so nothing lies between them. This is not an error, and a program that builds substrings in a loop will meet it sooner or later.

expecting substring(1, 4) to give four characters What students write:

String code = "RJ14CA";
String part = code.substring(1, 4);
System.out.println(part);

What happens: it prints J14, three characters, not four. The end index is not included, so the run stops just before index 4.

The fix:

String part = code.substring(1, 5);

To take n characters starting at index b, write substring(b, b + n). Counting arguments is guesswork; counting cuts is arithmetic.

Fixed-format codes are the everyday use of this method. A school stores each book of its library under a code such as MATH-2026-014, in which the first four characters name the subject, the next four give the year of purchase and the last three are the serial number. The positions never move, so the parts can be lifted out by index alone.

Splitting a fixed-format library book code Problem: A library book code has the form SUBJ-YYYY-NNN, with a subject of four letters, a four-digit year and a three-digit serial number, separated by hyphens. Take one such code, fixed inside the program, and display the three parts separately.

class BookCode
{
    public static void main(String args[])
    {
        String code = "MATH-2026-014";
        String subject = code.substring(0, 4);
        String year = code.substring(5, 9);
        String serial = code.substring(10);
        System.out.println("Code    : " + code);
        System.out.println("Subject : " + subject);
        System.out.println("Year    : " + year);
        System.out.println("Serial  : " + serial);
    }
}

Sample output:

Code    : MATH-2026-014
Subject : MATH
Year    : 2026
Serial  : 014

Variable description:

[Table:] Table 7.10 – variables used in BookCode [/Table] | Variable | Type | Purpose | |—|—|—| | code | String | the complete book code | | subject | String | the four letters naming the subject | | year | String | the four digits of the year of purchase | | serial | String | the three digits of the serial number |

Notes: The hyphens at indexes 4 and 9 are never taken, because each cut is placed just past them. The last part uses the one-argument form, since it runs to the end of the code; writing substring(10, 13) would give the same answer but would break the moment a serial number gained a digit. The year is still a piece of text here, not a number — turning it into one is the business of section 7.6.

7.4.3 Joining Strings

Two strings are joined — concatenated — into a longer one either with the + operator or with the concat() method.

String first = "Computer";
String second = "Science";
System.out.println(first + " " + second);
System.out.println(first.concat(" ").concat(second));

Both lines print Computer Science.

The + operator is far more common, for two reasons. It reads naturally when several pieces are joined at once, and it accepts values that are not strings at all — a number, a character or a boolean on either side of a string is turned into text automatically. That is why every System.out.println in this chapter can join a heading to a count without any conversion being written.

int books = 37;
System.out.println("Books issued: " + books);

The concat() method has no such generosity. It takes a String and nothing else, so first.concat(37) will not compile.

One habit is worth forming now. Since each join builds a new object, a program that joins in a loop pays the cost measured in section 7.3.3. For a handful of pieces this does not matter at all; for thousands, section 7.7 has the answer.

7.4.4 Changing Case and Removing Blanks

Three methods tidy a string up.

toUpperCase() returns the string with every small letter turned into a capital. toLowerCase() does the reverse. Characters that are not letters — digits, punctuation, spaces — pass through untouched.

trim() returns the string with whitespace removed from the front and the back. Blanks between words are left exactly as they were.

String entry = "   Sitaram Verma   ";
System.out.println("[" + entry.trim() + "]");
System.out.println(entry.trim().toUpperCase());

prints

[Sitaram Verma]
SITARAM VERMA

The square brackets are printed only to make the missing blanks visible; they are not part of the string.

These three methods matter more than they look. Text typed by a person arrives with stray spaces at the ends and in whatever case the typist felt like using. Comparing such text to anything stored in the program is hopeless until it has been trimmed and put into one case — which is the subject of the next subsection.

7.4.5 Comparing Two Strings

Java offers three sensible ways to compare strings, and one tempting way that is wrong.

equals() returns true when the two strings hold exactly the same characters in the same order, treating capitals and small letters as different.

equalsIgnoreCase() asks the same question while treating 'A' and 'a' as matching.

compareTo() answers a bigger question: which of the two comes first. It returns 0 when the strings are identical, a negative number when the string it is called on comes first, and a positive number when it comes later.

String a = "AMAN";
String b = "AMIT";
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));

prints

false
-8

Where does −8 come from? compareTo() walks along both strings together. 'A' matches 'A', 'M' matches 'M', and at index 2 it finds 'A' against 'I'. It stops there and returns the difference of their codes, 65 − 73, which is −8. If one string runs out first while everything has matched — as with "car" and "cart" — it returns the difference of the lengths instead.

The sign is what your program should use; the exact size is rarely of interest. A negative result means “comes first in dictionary order”, and since capitals carry smaller codes than small letters, "Zebra" comes before "apple" in Java’s ordering even though it comes later in an English dictionary.

Table 7.11 – four ways of comparing two strings

Feature == equals() equalsIgnoreCase() compareTo()
Question asked same object? same characters? same characters, ignoring case? which comes first?
Type of result boolean boolean boolean int
Treats 'A' and 'a' as — different same different
Correct for testing equality of text no yes yes, when case is to be ignored yes, by testing for 0
Usable for sorting no no no yes

comparing two strings with == What students write:

String typed = new String("yes");
if (typed == "yes")
{
    System.out.println("matched");
}

What happens: nothing prints. The == operator asks whether the two variables refer to the same object, and new deliberately built a second object, as section 7.3.2 showed. The characters are identical; the objects are not. What makes this error dangerous is that the same code with a plain literal instead of new would have printed matched, so a program can pass every test you try and fail on real input.

The fix:

String typed = new String("yes");
if (typed.equals("yes"))
{
    System.out.println("matched");
}

Compare the contents with equals(), or with equalsIgnoreCase() when the case of the typed answer should not matter. Reserve == for primitive values.

7.4.6 Searching Inside a String

Four methods answer “where is it?”, and two answer the simpler “does it start or end with it?”.

Table 7.12 – methods that search a string, with what each returns

Method call Returns
s.indexOf(ch) index of the first occurrence of the character ch, or −1
s.indexOf(ch, from) index of the first occurrence at or after index from, or −1
s.indexOf(t) index where the string t first occurs, or −1
s.indexOf(t, from) index where t first occurs at or after index from, or −1
s.lastIndexOf(ch) index of the last occurrence of ch, or −1
s.lastIndexOf(t) index where t last occurs, or −1
s.startsWith(t) true if the string begins with t
s.endsWith(t) true if the string ends with t

Every searching method returns −1 when the thing being looked for is not there. That value is not an index and can never be mistaken for one, which is what makes it a safe answer to “nowhere”.

String s = "Computer Science";
System.out.println(s.indexOf('c'));
System.out.println(s.lastIndexOf('c'));
System.out.println(s.indexOf("Science"));
System.out.println(s.indexOf('z'));

prints

10
14
9
-1

Note the first two lines carefully. The capital C at index 0 is not found by a search for the small c, because searching is case sensitive like everything else here.

The version taking a starting index is the important one, because it turns a single search into a repeated one. Search from 0, note where you found it, then search again from just after that position. Repeat until the answer is −1. Fig. 7.4-2 shows the idea on a short sentence.

Diagram

Fig. 7.4-2 – the sentence “the bus to the fort leaves the stand” written along a line with index numbers 0, 11 and 27 marked beneath the three places where “the” begins; three curved arrows drawn above the line, the first from the left end to position 0 labelled “search from 0”, the second from position 1 to position 11 labelled “search from 1”, the third from position 12 to position 27 labelled “search from 12”; a fourth arrow from position 28 running off the right end labelled “search from 28 gives −1”

counting the occurrences of a word in a sentence Input: a sentence, and a word to look for; the word is not empty. Output: the number of places at which the word occurs in the sentence.

Method:

1. Set count to 0.
2. Set pos to the index of the first occurrence of the word in the sentence.
3. While pos is not -1, repeat steps 4 and 5.
4.    Add 1 to count.
5.    Set pos to the index of the next occurrence, searching from pos + 1.
6. Report count.

Why it works: each turn of the loop counts one occurrence and then restarts the search strictly to the right of it, so no occurrence can be counted twice and none can be skipped. The search position increases every turn, so it must eventually pass the end of the sentence and produce −1, which stops the loop.

Cost: let \(n\) be the number of characters in the sentence and \(m\) the number in the word. Each search compares at most \(m\) characters at each of at most \(n\) starting positions, so the work is \(O(n \times m)\) in the worst case.

Counting how often a word occurs in a sentence Problem: A sentence and a word are fixed inside the program. Display the sentence, the word, and the number of times the word occurs in it.

class WordCount
{
    public static void main(String args[])
    {
        String text = "the bus to the fort leaves the stand";
        String word = "the";
        int count = 0;
        int pos = text.indexOf(word);
        while (pos != -1)
        {
            count = count + 1;
            pos = text.indexOf(word, pos + 1);
        }
        System.out.println("Sentence : " + text);
        System.out.println("Word     : " + word);
        System.out.println("Found    : " + count + " times");
    }
}

Sample output:

Sentence : the bus to the fort leaves the stand
Word     : the
Found    : 3 times

Variable description:

[Table:] Table 7.13 – variables used in WordCount [/Table] | Variable | Type | Purpose | |—|—|—| | text | String | the sentence being searched | | word | String | the word being looked for | | count | int | number of occurrences found so far | | pos | int | index of the current occurrence, or −1 when none remains |

Notes: The search restarts at pos + 1 and not at pos, or the same occurrence would be found for ever and the program would never stop. This method counts occurrences of the characters, not of the word as a word — searching for "the" in "theatre" would find one. Section 7.8 shows how to count whole words instead.

WordCount on the sentence shown above, searching for “the”

Turn pos on entering the loop count after step 4 pos after searching from pos + 1
1 0 1 11
2 11 2 27
3 27 3 −1

Result: the loop ends with count equal to 3 and the program prints 3. The trace shows the search position moving strictly rightwards, which is the reason the loop is guaranteed to finish.

7.4.7 Replacing Characters and Substrings

The method replace() returns a new string in which every occurrence of one thing has been swapped for another. It comes in two forms: one that replaces a character, and one that replaces a piece of text.

String s = "MXLXYXLXM";
System.out.println(s.replace('X', 'A'));

String t = "the green bottle in the green bag";
System.out.println(t.replace("green", "red"));

prints

MALAYALAM
the red bottle in the red bag

Two points, both easy to miss. Replacement happens at every occurrence, not just the first — there is no version that changes only one. And, as always, the original string is untouched; s still holds "MXLXYXLXM" after that first line has run.

The form taking two pieces of text is available from Java 5.0 onwards, which is the version this book assumes.

Table 7.14 gathers the methods of this section in one place, for use while you write the programs of the sections that follow.

Table 7.14 – the String methods used in this chapter, with the type of value each returns

Method call Returns What it gives
s.length() int number of characters in s
s.charAt(i) char the character at index i
s.substring(b) String characters from index b to the end
s.substring(b, e) String characters from index b up to, but not including, e
s.concat(t) String s followed by t
s.toUpperCase() String s with letters in capitals
s.toLowerCase() String s with letters in small case
s.trim() String s without leading and trailing whitespace
s.equals(t) boolean whether the characters are identical
s.equalsIgnoreCase(t) boolean whether they are identical ignoring case
s.compareTo(t) int 0, negative or positive, giving dictionary order
s.indexOf(x) int first index of x, or −1
s.indexOf(x, from) int first index of x at or after from, or −1
s.lastIndexOf(x) int last index of x, or −1
s.startsWith(t) boolean whether s begins with t
s.endsWith(t) boolean whether s ends with t
s.replace(a, b) String s with every a replaced by b

Take the sentence used in WordCount and change the word being searched for to "to". Predict the count before you run it, then run it and explain the answer you get. Now change the sentence so that one occurrence sits inside a longer word, and say what this tells you about the difference between finding characters and finding words.

7.5 Algorithms on Strings

You now have the whole toolkit: a way to reach any character, a way to cut out a run of them, a way to join, compare, search and replace. What you do not yet have is a way of thinking about string problems, and that is what this section is for.

Almost every string problem set at this level is one of three patterns, or a combination of them. Learn the three patterns and you will rarely have to invent an approach from nothing.

7.5.1 Scanning a String Character by Character

The first pattern examines every character in turn and keeps a running answer. Counting problems, testing problems and searching problems are all of this shape.

The loop is always the same:

for (int i = 0; i < s.length(); i++)
{
    char ch = s.charAt(i);
}

What changes from problem to problem is only what happens to ch inside the loop.

Consider counting vowels and consonants in a line of text. Two decisions have to be made about each character: first, is it a letter at all, and second, if it is, is it one of the five vowels? Punctuation, digits and blanks belong in neither count and must be passed over.

For the second test there is a neat trick worth learning. Put the five vowels in a string and ask whether the character occurs in it:

if ("aeiou".indexOf(ch) >= 0)

Since indexOf() returns −1 when the character is absent, a result of 0 or more means “yes, it is one of these”. This is indexOf() used not to find a position but to test membership, and it saves writing five comparisons joined by ||.

Converting the character to small case first means the test has to list only five vowels instead of ten.

counting vowels and consonants in a line of text Input: a line of text, which may contain letters, digits, punctuation and blanks. Output: the number of vowels and the number of consonants in it.

Method:

1. Set vowels to 0 and consonants to 0.
2. For each index i from 0 up to one less than the length of the line, repeat steps 3 to 5.
3.    Take the character at index i and convert it to small case; call it ch.
4.    If ch is not a letter, do nothing further for this character.
5.    Otherwise, if ch is one of a, e, i, o, u then add 1 to vowels, else add 1 to consonants.
6. Report vowels and consonants.

Why it works: every character is examined exactly once, and each one increases at most one of the two counts, so no letter can be counted twice and no letter can be missed.

Cost: let \(n\) be the number of characters in the line. The loop runs \(n\) times and does a fixed amount of work each turn, so the cost is \(O(n)\).

Counting vowels and consonants in a line typed by the user Problem: Read one line of text from the keyboard and display how many vowels and how many consonants it contains. Characters that are not letters are ignored, and capital and small letters are counted alike.

import java.util.Scanner;

class VowelCount
{
    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter a line of text:");
        String line = in.nextLine();
        int vowels = 0;
        int consonants = 0;
        for (int i = 0; i < line.length(); i++)
        {
            char ch = Character.toLowerCase(line.charAt(i));
            if (Character.isLetter(ch))
            {
                if ("aeiou".indexOf(ch) >= 0)
                {
                    vowels = vowels + 1;
                }
                else
                {
                    consonants = consonants + 1;
                }
            }
        }
        System.out.println("Vowels     : " + vowels);
        System.out.println("Consonants : " + consonants);
    }
}

Sample output, with the second line typed by the user:

Enter a line of text:
Jaipur is pink
Vowels     : 5
Consonants : 7

Variable description:

[Table:] Table 7.15 – variables used in VowelCount [/Table] | Variable | Type | Purpose | |—|—|—| | in | Scanner | reads the line typed at the keyboard | | line | String | the line of text being examined | | i | int | index of the character being examined | | ch | char | the current character, in small case | | vowels | int | running count of vowels | | consonants | int | running count of consonants |

Notes: nextLine() is used rather than next() because the input contains blanks and the whole line is wanted. Converting to small case once, at the moment the character is taken, removes every later worry about case — a small decision that keeps the two tests short. The two blanks in the sample input raise neither count, which is exactly what the specification asked for.

VowelCount on the input Ram!

i character taken ch after conversion is it a letter? vowels consonants
0 'R' 'r' yes 0 1
1 'a' 'a' yes 1 1
2 'm' 'm' yes 1 2
3 '!' '!' no 1 2

Result: the program prints 1 vowel and 2 consonants. The last row is the one to notice — a character that fails the letter test leaves both counts exactly as they were.

7.5.2 Building a New String from an Old One

The second pattern does not count anything. It walks through the old string and assembles a new one, deciding at each step what to add.

The shape is always this:

String result = "";
for (int i = 0; i < s.length(); i++)
{
    result = result + s.charAt(i);
}

with the order of the walk, or the condition on what gets added, changed to suit the problem. Fig. 7.5-1 shows the two strings involved: one being read, one growing.

Diagram

Fig. 7.5-1 – two rows of boxes; the upper row is the source string with an arrow beneath it labelled “index i moves left to right”, the lower row is the result string shown shorter, with a dashed box at its right end labelled “character added here”; between the rows draw a curved arrow from the box at index i to the dashed box, labelled “add if the condition is satisfied”

Reversing a string is the simplest use. Walk the source from its last index down to 0, adding each character to the result. The result then holds the characters in the opposite order.

reversing a string Input: a string. Output: a new string with the same characters in the opposite order.

Method:

1. Set result to the empty string.
2. For each index i from one less than the length of the string down to 0, repeat step 3.
3.    Add the character at index i to the end of result.
4. Report result.

Why it works: the characters are added in order of decreasing index, so the character that was last becomes the first added, and the character that was first is added last. Every character is added exactly once.

Cost: let \(n\) be the number of characters. The loop turns \(n\) times, but each join copies the result built so far, so the total work is \(O(n^2)\) rather than \(O(n)\) — the cost measured in section 7.3.3.

A word that reads the same in both directions is a palindrome, and once a string can be reversed the test is one comparison.

Reversing a word and deciding whether it is a palindrome Problem: Read one word from the keyboard, display it reversed, and state whether it is a palindrome. Capital and small letters are to be treated as the same letter.

import java.util.Scanner;

class Palindrome
{
    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter a word:");
        String word = in.next();
        String reversed = "";
        for (int i = word.length() - 1; i >= 0; i--)
        {
            reversed = reversed + word.charAt(i);
        }
        System.out.println("Reversed : " + reversed);
        if (word.equalsIgnoreCase(reversed))
        {
            System.out.println("The word is a palindrome");
        }
        else
        {
            System.out.println("The word is not a palindrome");
        }
    }
}

Sample output, with the second line typed by the user:

Enter a word:
Malayalam
Reversed : malayalaM
The word is a palindrome

Variable description:

[Table:] Table 7.16 – variables used in Palindrome [/Table] | Variable | Type | Purpose | |—|—|—| | in | Scanner | reads the word typed at the keyboard | | word | String | the word entered by the user | | i | int | index of the character being copied | | reversed | String | the word being built in reverse order |

Notes: The loop counts downwards, so it starts at word.length() - 1 — the last index, not the length — and stops when i reaches 0, which is why the condition is i >= 0 and not i > 0. The comparison uses equalsIgnoreCase() rather than equals(), and the sample output shows exactly why: reversing "Malayalam" produces "malayalaM", and a case-sensitive test would call this word no palindrome at all.

Q. The program builds the whole reversed word before comparing. Could the test be done without building anything?

A. Yes. Compare the first character with the last, the second with the second-last, and so on until the two positions meet; if any pair disagrees the word is not a palindrome. That version does no joining at all, so it costs \(O(n)\) instead of \(O(n^2)\) and needs no second string.

7.5.3 Working with the Words of a Sentence

The third pattern treats a sentence not as characters but as words. The words are separated by blanks, so the method is to collect characters until a blank arrives, then deal with the word that has been collected and start an empty one.

Two details make the difference between a program that works and one that nearly works.

The first is the last word. A sentence usually ends with a letter, not a blank, so a program that acts only when it meets a blank will collect the final word and never process it. The cure is to add one blank to the end of the sentence before the scan begins, so that every word — including the last — is followed by one.

The second is repeated blanks. If a sentence contains two spaces between words, the second blank arrives when the collected word is still empty. Processing it would produce a word of no characters, so the program must check that a word has actually been collected before dealing with it.

Fig. 7.5-2 shows the collecting variable filling and emptying as the scan moves along.

Diagram

Fig. 7.5-2 – the sentence “Go now” written as a row of seven boxes with indexes 0 to 6 below them; beneath the row, seven snapshots of a small box labelled “word”, showing in turn G, Go, empty, n, no, now, empty, each snapshot linked by a dotted line to the index that produced it; mark the two blank positions with the label “word is complete: use it, then empty it”

extracting the words of a sentence Input: a sentence whose words are separated by one or more blanks. Output: each word of the sentence, taken one at a time.

Method:

1. Remove blanks from both ends of the sentence and add a single blank at the end.
2. Set word to the empty string.
3. For each index i from 0 up to one less than the length of the sentence, repeat steps 4 and 5.
4.    Take the character at index i.
5.    If it is not a blank, add it to the end of word;
      otherwise, if word is not empty, deal with word and then set word to the empty string.
6. Stop.

Why it works: a character is either part of a word or a separator, and the two cases are handled separately. Adding a blank at the end guarantees that the final word meets a separator, so no word is left uncollected. The emptiness test means that extra blanks produce no words.

Cost: let \(n\) be the number of characters in the sentence. Each character is examined once, so the scan itself is \(O(n)\).

Displaying each word of a sentence with its number of letters Problem: Read a sentence from the keyboard and display each word on a line of its own, followed by the number of characters in that word, written in brackets. Extra blanks between words are to be ignored.

import java.util.Scanner;

class WordLengths
{
    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter a sentence:");
        String line = in.nextLine().trim() + " ";
        String word = "";
        for (int i = 0; i < line.length(); i++)
        {
            char ch = line.charAt(i);
            if (ch != ' ')
            {
                word = word + ch;
            }
            else
            {
                if (word.length() > 0)
                {
                    System.out.println(word + " (" + word.length() + ")");
                    word = "";
                }
            }
        }
    }
}

Sample output, with the second line typed by the user:

Enter a sentence:
Reading books is a good habit
Reading (7)
books (5)
is (2)
a (1)
good (4)
habit (5)

Variable description:

[Table:] Table 7.17 – variables used in WordLengths [/Table] | Variable | Type | Purpose | |—|—|—| | in | Scanner | reads the sentence typed at the keyboard | | line | String | the trimmed sentence, with one blank added at the end | | i | int | index of the character being examined | | ch | char | the current character | | word | String | the characters of the word collected so far |

Notes: The whole preparation of the input is done in one statement — trim() removes stray blanks at the ends, and + " " supplies the single blank that closes the last word. Emptying word immediately after use is what allows the same variable to serve every word in the sentence; forget that line and the second word arrives already carrying the first.

WordLengths on the sentence Go now

i ch action taken word after the action
0 'G' added to word "G"
1 'o' added to word "Go"
2 ' ' printed Go (2), emptied word ""
3 'n' added to word "n"
4 'o' added to word "no"
5 'w' added to word "now"
6 ' ' printed now (3), emptied word ""

Result: the program prints two lines. The trace shows that the seventh character, the blank added by the program itself, is what causes the last word to be printed at all.

losing the last word of the sentence What students write:

String line = in.nextLine();
String word = "";
for (int i = 0; i < line.length(); i++)
{
    char ch = line.charAt(i);
    if (ch != ' ')
    {
        word = word + ch;
    }
    else
    {
        System.out.println(word);
        word = "";
    }
}

What happens: for the sentence Reading books is a good habit, five words print and habit never appears. The loop ends the moment the last character is consumed, and word is still holding the final word with nobody to print it.

The fix:

String line = in.nextLine().trim() + " ";

Add one blank at the end of the sentence, so that the last word meets a separator like every other word. The alternative — printing whatever remains in word after the loop — works too, but it means writing the same three lines twice.

Practical link: this assignment exercises scanning a sentence word by word (sections 7.5.1 and 7.5.3); file it in the work file under string handling. Write a program that reads a sentence terminated by a full stop and displays each word on a separate line together with the number of vowels in that word, ignoring the full stop itself. Words may be separated by more than one blank, and the sentence may be typed in any mixture of capital and small letters. Test it on Study well and rest well. and then on the unseen input A cup of tea., whose repeated blanks must not produce empty words.

7.6 Conversion Between Strings and Primitive Types

Section 7.1 began with a clerk typing 450 at a fee counter, and with the awkward fact that a program cannot multiply text. It is time to settle that.

Everything that arrives from a keyboard, a file or a code printed on a book arrives as characters. Everything that is displayed leaves as characters too. Numbers exist only in between. So a program spends a surprising amount of its life turning text into numbers and numbers back into text, and Java puts both jobs in the wrapper classes of section 7.1.

Fig. 7.6-1 shows the two directions and the methods that travel them.

Diagram

Fig. 7.6-1 – two boxes side by side, the left one labelled String "450" drawn as three character boxes 4, 5, 0, the right one labelled int 450 drawn as a single value box; an upper arrow from left to right labelled Integer.parseInt, and a lower arrow from right to left labelled Integer.toString and String.valueOf; under the left box write “characters, can be joined and searched”, under the right box write “a quantity, can be added and multiplied”

7.6.1 From a String to a Primitive Value

Reading a piece of text and working out the value it stands for is called parsing. Each numeric wrapper class carries a method that parses text into its own primitive type.

Table 7.18 – methods that convert a string into a primitive value

Method call Returns Example
Integer.parseInt(s) int Integer.parseInt("24") gives 24
Long.parseLong(s) long Long.parseLong("2465783") gives 2465783
Float.parseFloat(s) float Float.parseFloat("24.35") gives 24.35
Double.parseDouble(s) double Double.parseDouble("24.5") gives 24.5

Each is called on the class, not on an object, so the class name is written in front of it. Each takes a String and hands back a plain value you can calculate with.

There is a second family of methods with a similar purpose and a different result. Integer.valueOf(s) also reads the text, but it returns an Integer object rather than an int.

Table 7.19 – parseInt() and valueOf()

Feature Integer.parseInt(s) Integer.valueOf(s)
What it returns an int, a primitive value an Integer, an object
Ready for arithmetic yes, immediately only after unboxing
Usable where an object is required no yes
Behaviour when the text is not a number throws NumberFormatException throws NumberFormatException

From Java 5.0 onwards the difference is easy to overlook, because unboxing will quietly turn the object into a value on assignment, so int n = Integer.valueOf("24"); compiles and works. Overlooking it is still a mistake: the object was built and then thrown away. When you want a number to calculate with, parseInt() says so plainly.

A common use is a value buried inside a longer piece of text, where the digits must first be cut out with substring() and only then parsed.

Finding the cost of three items from a printed price Problem: A price is stored as printed text of the form Rs.450, in which the first three characters are the label and the rest are the digits of the price. Take one such entry, fixed inside the program, and display the price as a number and the cost of three items at that price.

class PriceTotal
{
    public static void main(String args[])
    {
        String entry = "Rs.450";
        String digits = entry.substring(3);
        int price = Integer.parseInt(digits);
        int total = price * 3;
        System.out.println("Entry        : " + entry);
        System.out.println("Price        : " + price);
        System.out.println("Cost of three: " + total);
    }
}

Sample output:

Entry        : Rs.450
Price        : 450
Cost of three: 1350

Variable description:

[Table:] Table 7.20 – variables used in PriceTotal [/Table] | Variable | Type | Purpose | |—|—|—| | entry | String | the price as printed, including its label | | digits | String | the digits of the price, cut out of the entry | | price | int | the price as a number | | total | int | the cost of three items |

Notes: The order of the two steps cannot be swapped. Integer.parseInt("Rs.450") would fail, because the letters are not part of any number — the text must be cut down to digits before it is parsed. Notice also that price and entry look alike in the output but behave quite differently: only one of them can be multiplied.

adding numbers that are still text What students write:

String a = "40";
String b = "5";
System.out.println(a + b);

What happens: it prints 405. The + operator between two strings joins them; it has no idea that these particular characters look like numbers. No error is reported, because nothing illegal has been done — the program simply answered a different question.

The fix:

String a = "40";
String b = "5";
System.out.println(Integer.parseInt(a) + Integer.parseInt(b));

Parse first, then add. The same + symbol means “join” between two strings and “add” between two numbers, so what it does depends entirely on what you hand it.

7.6.2 From a Primitive Value to a String

The journey back is easier, because Java is generous about it. Three ways are available, and all three give the same result.

Table 7.21 – ways of converting a primitive value into a string

Way of writing it Example Result
Integer.toString(n) Integer.toString(45) "45"
String.valueOf(n) String.valueOf(45) "45"
joining to an empty string "" + 45 "45"

Long.toString(), Float.toString() and Double.toString() do the same for the other numeric types, while String.valueOf() accepts a value of any type at all, which is why it is often the shortest thing to write.

The third way explains something you have been using since the first program of this chapter. When System.out.println("Price : " + price) runs, the number on the right of the + is converted to text automatically, because the value on the left is a string. You have been converting numbers all along without noticing.

Why convert deliberately, then? Because once a number is text, the whole toolkit of section 7.4 becomes available to it. Counting the digits of a number, for instance, needs no arithmetic at all:

int n = 45678;
String s = Integer.toString(n);
System.out.println("The number has " + s.length() + " digits");

prints

The number has 5 digits

Testing whether a number reads the same backwards, or checking whether a particular digit occurs in it, becomes just as easy — a string question rather than a division-and-remainder question.

Q. Why does System.out.println("Total: " + 4 + 5); print Total: 45?

A. Because + is worked out from left to right. "Total: " + 4 gives the string "Total: 4", and joining 5 to that gives "Total: 45". Write + (4 + 5) and the addition happens first, giving Total: 9.

7.6.3 When the String Is Not a Number

Parsing can fail, and it fails loudly. If the text does not spell a proper number, the parsing method throws a NumberFormatException and the program stops at that line.

All of these fail:

Integer.parseInt("twelve");
Integer.parseInt("12a");
Integer.parseInt("45.7");
Integer.parseInt(" 45");
Integer.parseInt("");

The last two deserve comment. "45.7" is a perfectly good number but not a whole one, so parseInt() refuses it while Double.parseDouble() accepts it happily. And " 45" fails on nothing worse than a blank space, which is exactly why input from a keyboard is passed through trim() before it is parsed.

A program that stops with an exception in front of a user has not handled the situation; it has merely discovered it. The better habit is to check the text first and parse only when the check passes.

deciding whether a string spells a whole number Input: a piece of text, with blanks already removed from both ends. Output: true if every character is a digit and the text is not empty, false otherwise.

Method:

1. If the text has no characters, report false and stop.
2. For each index i from 0 up to one less than the length of the text, repeat step 3.
3.    If the character at index i is not a digit, remember that the text is not a number.
4. Report whether the text is still considered a number.

Why it works: a whole number written in text is exactly a non-empty run of digit characters, so the test examines precisely the condition that parseInt() requires. Since the empty string is rejected in step 1, no text that passes the check can fail to parse.

Cost: let \(n\) be the number of characters in the text. Each character is examined once, so the check costs \(O(n)\).

Checking typed text before converting it to a number Problem: Read a line from the keyboard, ignoring blanks at either end. If the line spells a whole number, display twice that number; otherwise display a message saying that it is not a whole number.

import java.util.Scanner;

class SafeNumber
{
    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter a whole number:");
        String entry = in.nextLine().trim();
        boolean valid = entry.length() > 0;
        for (int i = 0; i < entry.length(); i++)
        {
            if (Character.isDigit(entry.charAt(i)) == false)
            {
                valid = false;
            }
        }
        if (valid == true)
        {
            int value = Integer.parseInt(entry);
            System.out.println("Twice the number is " + (value * 2));
        }
        else
        {
            System.out.println("That is not a whole number");
        }
    }
}

Sample output, with the second line typed by the user:

Enter a whole number:
76
Twice the number is 152

Variable description:

[Table:] Table 7.22 – variables used in SafeNumber [/Table] | Variable | Type | Purpose | |—|—|—| | in | Scanner | reads the line typed at the keyboard | | entry | String | the typed text, with end blanks removed | | i | int | index of the character being checked | | valid | boolean | whether the text so far spells a whole number | | value | int | the number obtained by parsing the text |

Notes: valid is set from the length test before the loop begins, so an empty line is rejected without any character being examined. The loop can only turn valid from true to false, never back, which is what makes a single bad character enough to condemn the whole line. Run the program again and type 76 rupees: the check catches it, and the program says so politely instead of stopping with an exception.

Checking input before trusting it is not extra work. It is the difference between a program that behaves predictably for every input and one that behaves well only for the inputs its author happened to try.

Change SafeNumber so that it accepts a number with a decimal point, such as 24.35, and displays twice its value. You will need Double.parseDouble() and a check that allows exactly one full stop among the digits. Test it on 24.35, on 24.3.5 and on a line containing only a full stop, and say what each one should do before you run it.

7.7 The StringBuffer Class

Twice in this chapter a cost has been noted and postponed. In section 7.3.3 you saw that joining strings makes a new object every time, and in section 7.5.2 the reversal algorithm carried a cost of \(O(n^2)\) for that reason alone.

This section pays the debt. Java provides a second class for holding text, built on the opposite principle, and knowing when to reach for it is part of writing programs that scale.

7.7.1 The Cost of Repeated String Building

Look once more at the line that does the damage:

result = result + s.charAt(i);

Because a String cannot be altered, this statement cannot add a character to result. It must build a new object holding everything result had plus one more character, point result at it, and abandon the old object. On the next turn it does the same thing again, with one more character to copy.

Building 10 characters costs about 55 copies, which nobody notices. Building 100,000 characters costs about five thousand million copies, which everybody notices. The work grows as the square of the length, while the text grows only in proportion to it.

The cure is an object that can be altered. Java calls it a StringBuffer.

NoteDefinition

A StringBuffer object holds a sequence of characters that can be altered in place — characters may be added, removed or replaced without any new object being created.

An object whose contents can be changed after it has been created is said to be mutable, the opposite of the immutability studied in section 7.3.3. A StringBuffer also keeps spare room at the end of its store, so that adding characters usually needs no copying at all. Fig. 7.7-1 contrasts the two ways of growing text.

Diagram

Fig. 7.7-1 – two panels; the upper panel shows four separate string objects in a row containing J, Ja, Jai and Jaip, with the first three crossed out and labelled “abandoned”, and an arrow from the last labelled “the variable now points here”; the lower panel shows one buffer drawn as a long row of cells with J, a, i, p in the first four and the rest empty, an arrow pointing at the fifth cell labelled “the next character goes here”, and the whole panel labelled “one object, altered in place”

adding characters to a StringBuffer Let \(n\) be the number of characters in the finished text. Adding one character copies only that character, not the ones already there, so building the whole text costs work in proportion to \(n\) — that is, \(O(n)\), against \(O(n^2)\) for the same job done with String. The buffer is occasionally enlarged when its spare room runs out, but that happens rarely enough not to change the dominant term.

Q4. A program builds a long sentence by joining one word at a time inside a loop. It works for ten words but crawls for ten thousand. What is going wrong, and which class repairs it?

A. Nothing is wrong with the logic — the cost is wrong. Because a String is immutable, every join copies the entire sentence built so far into a new object, so the total work grows as the square of the length. Ten words hide this completely; ten thousand words make it the whole running time. Building the sentence in a StringBuffer instead, and converting it to a String once at the end, brings the cost back down to a straight line.

7.7.2 Creating a StringBuffer Object

A StringBuffer has no literal form. There is no way to write one between quotes, so every one of them is made with new.

Three constructors are used:

StringBuffer a = new StringBuffer();
StringBuffer b = new StringBuffer(40);
StringBuffer c = new StringBuffer("Jaipur");

The first creates an empty buffer with room for 16 characters. The second creates an empty buffer with room for as many characters as you ask for. The third creates a buffer already holding the given text, with 16 characters of spare room beyond it.

Two different measurements of a buffer follow from this, and they are easy to confuse. The length is the number of characters actually held. The capacity is the number the buffer has room for before it must be enlarged. Fig. 7.7-2 shows both for the third buffer above.

Diagram

Fig. 7.7-2 – a long row of 22 cells; the first six contain J, a, i, p, u, r and are shaded, the remaining sixteen are empty; a bracket under the first six labelled “length = 6”, a bracket under the whole row labelled “capacity = 22”, and a note beside the empty cells reading “spare room: 16 characters”

Showing the length and capacity of three buffers Problem: Create three StringBuffer objects — one empty with the default room, one empty with room for 40 characters, and one holding the word Jaipur — and display the length and capacity of each.

class BufferSpace
{
    public static void main(String args[])
    {
        StringBuffer a = new StringBuffer();
        StringBuffer b = new StringBuffer(40);
        StringBuffer c = new StringBuffer("Jaipur");
        System.out.println("a : length " + a.length() + ", capacity " + a.capacity());
        System.out.println("b : length " + b.length() + ", capacity " + b.capacity());
        System.out.println("c : length " + c.length() + ", capacity " + c.capacity());
    }
}

Sample output:

a : length 0, capacity 16
b : length 0, capacity 40
c : length 6, capacity 22

Variable description:

[Table:] Table 7.23 – variables used in BufferSpace [/Table] | Variable | Type | Purpose | |—|—|—| | a | StringBuffer | an empty buffer with the default room | | b | StringBuffer | an empty buffer with room asked for in advance | | c | StringBuffer | a buffer created from a word |

Notes: The first two buffers hold nothing, so their length is 0 however much room they have — capacity is a promise about the future, not a count of the present. The third line shows the rule for the text constructor: 6 characters held, 16 spare, 22 in all. Asking for room in advance, as b does, is worth doing when you know roughly how much text is coming, since it saves the buffer from being enlarged as it fills.

assigning text to a StringBuffer as if it were a String What students write:

StringBuffer name = "Jaipur";

What happens: the program does not compile. The compiler reports that a String cannot be converted to a StringBuffer. A quoted literal always creates a String, and no automatic conversion exists between the two classes.

The fix:

StringBuffer name = new StringBuffer("Jaipur");

Pass the text to the constructor. The reverse journey, from buffer back to string, is made by calling toString() on the buffer — never by assignment either.

7.7.3 Operations on a StringBuffer

The methods of StringBuffer do what their names say, and — this is the whole point of the class — they do it to the object they are called on.

Table 7.24 – methods of the StringBuffer class used in this chapter

Method call Effect on the buffer b
b.append(x) adds x at the end; x may be text, a number or a character
b.insert(i, x) inserts x so that it begins at index i
b.delete(i, j) removes the characters from index i up to, but not including, j
b.setCharAt(i, ch) replaces the character at index i by ch
b.reverse() reverses the order of all the characters
b.setLength(n) makes the length exactly n, cutting or padding as needed
b.charAt(i) returns the character at index i; the buffer is unchanged
b.length() returns the number of characters held
b.capacity() returns the room currently available
b.toString() returns the contents as a String; the buffer is unchanged

Three short examples show the difference from String most clearly. In each, the buffer itself changes and no result has to be caught:

StringBuffer s = new StringBuffer("COMMUTER");
s.setCharAt(3, 'P');
System.out.println(s);

StringBuffer t = new StringBuffer("COMPUTER");
t.delete(3, 5);
System.out.println(t);

StringBuffer u = new StringBuffer("STRESSED");
u.reverse();
System.out.println(u);

prints

COMPUTER
COMTER
DESSERTS

The delete() call removes the characters at indexes 3 and 4 — P and U — because the second index marks where the removal stops, exactly as it does in substring().

Q. What does System.out.println(s); do when s is a StringBuffer?

A. It calls toString() on the buffer and prints the text that comes back. You need to write toString() yourself only when the text must be stored in a String variable or passed to a method that expects one.

Now put the class to its proper use: building text a piece at a time.

Building a one-line sales summary in a buffer Problem: The day’s sales figures for four counters are held in an array. Build a single line of the form Sales: 120, 85, 200, 45 in a StringBuffer, then display the line and the number of characters in it.

class SalesLine
{
    public static void main(String args[])
    {
        int sales[] = {120, 85, 200, 45};
        StringBuffer line = new StringBuffer();
        for (int i = 0; i < sales.length; i++)
        {
            line.append(sales[i]);
            if (i < sales.length - 1)
            {
                line.append(", ");
            }
        }
        line.insert(0, "Sales: ");
        System.out.println(line);
        System.out.println("Characters: " + line.length());
    }
}

Sample output:

Sales: 120, 85, 200, 45
Characters: 23

Variable description:

[Table:] Table 7.25 – variables used in SalesLine [/Table] | Variable | Type | Purpose | |—|—|—| | sales | int[] | the four sales figures | | line | StringBuffer | the summary line being built | | i | int | index of the figure being added |

Notes: append() accepts an int directly and converts it to text on the way in, so no call to Integer.toString() is needed. The separator is added only when another figure is still to come, which is why the test is i < sales.length - 1; without it the line would end in a stray comma. The heading is added at the front at the end, by insert(), rather than before the loop — either order works here, and putting it last keeps the loop concerned with one thing only.

SalesLine on the figures 120, 85, 200, 45

i after append(sales[i]) separator added? contents of line
0 120 yes 120,
1 85 yes 120, 85,
2 200 yes 120, 85, 200,
3 45 no 120, 85, 200, 45

Result: after the loop the buffer holds 120, 85, 200, 45; insert() then puts the heading in front of it, giving a line of 23 characters. Notice that the same buffer appears in every row — there is only ever one object, growing.

7.7.4 Choosing Between String and StringBuffer

Both classes hold text and both offer methods for working with it, so the choice can look arbitrary. It is not.

Table 7.26 – String and StringBuffer

Feature String StringBuffer
Contents after creation can never change can be altered in place
Created by a quoted literal yes no, always with new
What a method such as toUpperCase() or append() gives back a new object; the original is untouched the same object, now altered
Cost of building \(n\) characters piece by piece \(O(n^2)\) \(O(n)\)
Spare room kept in advance none yes, reported by capacity()
Safe to share between parts of a program yes, since nobody can alter it no, since anyone holding it can alter it
Suited to text that is read, compared and searched text that is assembled or edited

The last two rows are the honest trade. Immutability is not a defect that StringBuffer repairs; it is a guarantee that StringBuffer gives up in exchange for speed. A String can be handed to any part of a program with no fear that it will come back changed. A StringBuffer carries no such promise.

So the working rule is simple. Use String for text you are examining, and StringBuffer for text you are building or editing — especially inside a loop. When the building is finished, call toString() once and go back to the safety of a String.

Rewrite the reversal loop of Palindrome from section 7.5.2 so that the reversed word is built in a StringBuffer and converted to a String only at the end. Then write the same reversal a third way, using the reverse() method in a single statement. Compare the three versions for length and for cost, and say which one you would submit and why.

7.8 The StringTokenizer Class

In section 7.5.3 you wrote out, by hand, the business of collecting characters until a blank arrives. It worked, but it needed three careful decisions — the added blank at the end, the emptying of the collecting variable, the guard against repeated blanks — and every one of them is a place to go wrong.

Splitting text into its parts is such a common job that Java supplies a class for it. This section is about that class, and about the vocabulary that goes with it.

7.8.1 Tokens, Whitespace and Delimiters

Start with the two words that matter.

A token is one meaningful piece of a larger piece of text — usually a word, a number or a code. A delimiter is a character that separates one token from the next and is not part of either.

NoteDefinition

A token is a run of consecutive characters treated as one unit, and a delimiter is a character that marks where one token ends and the next begins; delimiters are not part of the tokens they separate.

In an ordinary sentence the delimiters are the whitespace characters you met in section 7.2.2 — the blank space, the tab and the newline. In a stored record they are often something else: a colon, a comma or a hyphen chosen by whoever designed the record.

Fig. 7.8-1 shows the same idea for both cases.

Diagram

Fig. 7.8-1 – two rows; the upper row shows the sentence “Reading improves the mind” with the three blank positions marked by small crosses labelled “delimiters, discarded” and the four words boxed and labelled “tokens”; the lower row shows the record “Idli:30:2” with the two colons marked by crosses labelled “delimiters, discarded” and the three parts boxed and labelled “tokens”

Notice what happens to the delimiters. They do their job of marking a boundary and are then thrown away; they never appear in any token. That is why splitting a sentence into words loses the spaces between them, and why a program that must rebuild the sentence has to put separators back itself.

The class that performs the splitting is StringTokenizer, and it lives in the package java.util. Unlike the wrapper classes and String, it is not imported automatically, so a program that uses it must begin with an import statement:

import java.util.StringTokenizer;

StringTokenizer is one of the oldest classes in the Java library, written when text handling was expected to be simple. It has stayed in java.util ever since, because a great deal of working code depends on it.

7.8.2 Creating a StringTokenizer Object

A tokenizer is created with new, and is given the text it has to break up.

StringTokenizer st = new StringTokenizer("Reading improves the mind");

With one argument, the delimiters are the whitespace characters: the blank space, the tab, the newline, the carriage return and the form feed. A run of several of them counts as one boundary, which is why repeated blanks give no trouble here — the very problem that needed a guard in section 7.5.3.

With two arguments, you choose the delimiters yourself by listing them in a second string.

StringTokenizer st = new StringTokenizer("rice:wheat:sugar", ":");

Every character in that second string is treated as a delimiter in its own right, so ", " means “a comma or a blank”, not “a comma followed by a blank”.

The object created this way is not a copy of the text and not a list of words. It is best pictured as a queue of tokens waiting to be handed out, one at a time, as Fig. 7.8-2 shows.

Diagram

Fig. 7.8-2 – a rectangle labelled “StringTokenizer” containing four boxes in a row reading Reading, improves, the, mind; an arrow leaving the left end labelled “nextToken() takes the front one”; below, three snapshots of the same rectangle holding first three boxes, then two, then none, each labelled with the value countTokens() would return: 3, 2, 0

7.8.3 Extracting Tokens One by One

Three methods do all the work.

Table 7.27 – methods of the StringTokenizer class

Method call Returns What it does
st.countTokens() int how many tokens are still waiting
st.hasMoreTokens() boolean whether at least one token is still waiting
st.nextToken() String the next token, which is then removed from the queue

The middle column of that table hides the one fact students most often miss. countTokens() reports the tokens that remain, not the tokens the text originally had. Take three of them out and it will answer with whatever is left.

The standard loop follows directly from the other two methods:

while (st.hasMoreTokens())
{
    String word = st.nextToken();
}

Ask whether anything is left; if so, take it. The loop ends by itself when the queue is empty, so no counting is needed and no word can be missed.

Q5. How can a program break a sentence into separate words without examining the sentence one character at a time?

A. By handing the sentence to a StringTokenizer. The object treats whitespace as the boundary between tokens, so the program can simply ask hasMoreTokens() and take each word with nextToken() — the scanning still happens, but inside the library rather than in your loop. The three awkward details of a hand-written scan disappear: repeated blanks count as one boundary, the last word needs no special treatment, and there is no collecting variable to remember to empty.

Tokens are just as useful for stored records as for sentences. A canteen keeps each line of its price list as an item name, a rate and a quantity, joined by colons, and a program has to take that line apart before it can calculate anything.

Working out the amount from a canteen record Problem: A record has the form item:rate:quantity, with the rate and quantity written as whole numbers. Take one such record, fixed inside the program, and display the item, the rate, the quantity and the amount payable.

import java.util.StringTokenizer;

class BillLine
{
    public static void main(String args[])
    {
        String record = "Idli:30:2";
        StringTokenizer st = new StringTokenizer(record, ":");
        String item = st.nextToken();
        int rate = Integer.parseInt(st.nextToken());
        int quantity = Integer.parseInt(st.nextToken());
        System.out.println("Item     : " + item);
        System.out.println("Rate     : " + rate);
        System.out.println("Quantity : " + quantity);
        System.out.println("Amount   : " + (rate * quantity));
    }
}

Sample output:

Item     : Idli
Rate     : 30
Quantity : 2
Amount   : 60

Variable description:

[Table:] Table 7.28 – variables used in BillLine [/Table] | Variable | Type | Purpose | |—|—|—| | record | String | the complete record, with colons between its parts | | st | StringTokenizer | breaks the record into its three parts | | item | String | the name of the item | | rate | int | the rate, converted from text | | quantity | int | the quantity, converted from text |

Notes: Because the number of parts is known in advance, nextToken() is called three times directly instead of in a loop. The two calls to Integer.parseInt() are the bridge built in section 7.6 — a tokenizer always hands back text, even when the text is entirely digits, and text cannot be multiplied. A record with a missing colon would leave the third call with nothing to take; guarding against that would mean testing countTokens() before trusting the record.

7.8.4 Solving Sentence Problems with Tokens

Most sentence problems set at this level have the same shape: take the words one at a time, ask something about each, and keep a running answer. Finding the longest word is the plainest example of the pattern.

finding the longest word in a sentence Input: a sentence whose words are separated by whitespace. Output: the number of words, and the longest word in the sentence.

Method:

1. Make a tokenizer for the sentence, using whitespace as the delimiter.
2. Set words to the number of tokens the tokenizer holds.
3. Set longest to the empty string.
4. While the tokenizer still has a token, repeat steps 5 and 6.
5.    Take the next token; call it word.
6.    If word has more characters than longest, set longest to word.
7. Report words and longest.

Why it works: longest always holds the longest word among those examined so far, since it is replaced only by a longer one. When the last word has been examined, that statement is true of the whole sentence. Starting from the empty string is safe because every real word is longer than nothing.

Cost: let \(n\) be the number of characters in the sentence. Each character is examined once while the sentence is being split, and comparing lengths costs a fixed amount per word, so the whole method is \(O(n)\).

The counting in step 2 has to be done before the loop, not after it. Once the loop has emptied the queue, there is nothing left to count.

Counting the words of a sentence and finding the longest Problem: Read a sentence from the keyboard and display how many words it contains and which of them has the most characters. If two words are equally long, the earlier one is to be reported.

import java.util.Scanner;
import java.util.StringTokenizer;

class LongestWord
{
    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter a sentence:");
        String line = in.nextLine();
        StringTokenizer st = new StringTokenizer(line);
        int words = st.countTokens();
        String longest = "";
        while (st.hasMoreTokens())
        {
            String word = st.nextToken();
            if (word.length() > longest.length())
            {
                longest = word;
            }
        }
        System.out.println("Words in the sentence : " + words);
        System.out.println("Longest word          : " + longest);
    }
}

Sample output, with the second line typed by the user:

Enter a sentence:
Reading improves the mind
Words in the sentence : 4
Longest word          : improves

Variable description:

[Table:] Table 7.29 – variables used in LongestWord [/Table] | Variable | Type | Purpose | |—|—|—| | in | Scanner | reads the sentence typed at the keyboard | | line | String | the sentence entered by the user | | st | StringTokenizer | supplies the words of the sentence one at a time | | words | int | the number of words, counted before any are taken | | word | String | the word currently being examined | | longest | String | the longest word found so far |

Notes: The test is > and not >=, which is what makes the program report the earlier of two equally long words — a later word of the same length never displaces the one already held. That single character is the whole of the tie-breaking rule the problem asked for.

LongestWord on the sentence Go to the fort

Token taken its length length of longest before is it longer? longest after
Go 2 0 yes Go
to 2 2 no Go
the 3 2 yes the
fort 4 3 yes fort

Result: words was set to 4 before the loop began, and the loop ends with longest holding fort. The second row shows the tie-breaking rule at work: to is exactly as long as Go and is therefore not accepted.

counting the tokens after the loop What students write:

StringTokenizer st = new StringTokenizer(line);
while (st.hasMoreTokens())
{
    System.out.println(st.nextToken());
}
System.out.println("Number of words = " + st.countTokens());

What happens: every word prints correctly and the count is reported as 0. The loop ran until the queue was empty, and countTokens() reports only what is still waiting. Nothing has gone wrong inside the class — the question was simply asked too late.

The fix:

StringTokenizer st = new StringTokenizer(line);
int words = st.countTokens();
while (st.hasMoreTokens())
{
    System.out.println(st.nextToken());
}
System.out.println("Number of words = " + words);

Count first, then consume. If you need the words a second time, make a fresh tokenizer from the same sentence — a tokenizer cannot be rewound.

Table 7.30 – scanning a sentence yourself and using a tokenizer

Feature Hand-written scan with charAt() StringTokenizer
What the loop handles one character at a time one whole word at a time
Repeated blanks between words must be guarded against treated as one boundary
The last word of the sentence needs a blank added, or handling after the loop needs nothing special
Choice of separator any test you care to write any set of single characters you list
Number of words known in advance only by counting yourself from countTokens()
Access to individual characters direct only after taking the token

Neither approach is the better one in general. When a problem is about whole words — counting them, ordering them, comparing them — the tokenizer removes most of the ways of going wrong. When a problem is about the characters inside the words, you will be scanning with charAt() in any case, and often the neatest programs do both: a tokenizer to reach each word, and a character scan inside it.

Change LongestWord so that it reports the shortest word instead. Decide first what shortest should start out as, since the empty string will no longer do, and explain your choice in one line of comment. Test it on a sentence in which two words are equally short, and check that the one you expect is reported.

7.9 Summary

Wrapper classes
  1. A primitive type is built into the language and a variable of such a type holds one plain value, with no methods and no constants attached to it.

  2. A wrapper class is a class whose object holds a single value of one primitive type and supplies the methods and constants that belong to that type. There is exactly one wrapper class for each of the eight primitive types: Byte for byte, Integer for int, Short for short, Long for long, Float for float, Double for double, Boolean for boolean and Character for char.

  3. All eight belong to java.lang and need no import. The six numeric wrappers are grouped under the class Number; Character and Boolean are not.

  4. Autoboxing is the automatic conversion of a primitive value into an object of its wrapper class; unboxing is the automatic conversion of a wrapper object back into the primitive value it holds. Both are performed by the compiler from Java 5.0 onwards.

  5. A wrapper object costs more memory and more time than a plain value, so the primitive type remains the right choice for ordinary arithmetic.

Characters and their codes
  1. Every character is stored as a number. Java uses Unicode, whose first 128 codes are those of ASCII — the American Standard Code for Information Interchange.

  2. The codes to remember are 32 for the blank space, 48 to 57 for the digits, 65 to 90 for the capital letters and 97 to 122 for the small letters. Each group is unbroken, and the same letter in the two cases differs by exactly 32.

  3. A digit character is not the number it shows. The value of a digit character is obtained by subtracting the code of '0' from it.

  4. Whitespace is any character that occupies space without printing a mark — the blank space, the tab and the newline. It is what separates one word from the next.

  5. The Character class supplies methods that test a character (whether it is a letter, a digit, a letter or digit, a capital, a small letter or whitespace) and methods that convert one (to capital, to small, or to a one-character string). Arithmetic on a character uses its code, so turning a code back into a character needs a type cast.

The String class
  1. A String object holds a fixed sequence of characters, numbered from 0 up to one less than the length. The last index is therefore always one less than the length, and a loop over a string must stop before the index reaches the length.

  2. A string may be created from a literal or with new. Identical literals share one stored object, while new always builds a fresh one — which is why two strings must never be compared with ==.

  3. A String is immutable: its contents can never be altered after it has been created. Every method that appears to change a string in fact returns a new string and leaves the original untouched, so the result has to be caught in a variable.

  4. The methods of the class answer six kinds of question: how long the string is and what character stands at an index; what run of characters lies between two positions; how two strings are joined; how they compare; where something occurs inside a string; and what the string looks like with one thing replaced by another.

  5. A substring is taken between two cut positions: the character at the beginning index is included and the character at the ending index is not, so the length of the result is the ending index minus the beginning index.

  6. Equality of contents is tested with equals(), or with equalsIgnoreCase() when case is to be ignored. Dictionary order is given by compareTo(), which returns 0 for identical strings, a negative number when the first comes earlier and a positive number when it comes later; the value returned is the difference of the first pair of differing codes, or the difference of the lengths when one string is a prefix of the other.

  7. Every searching method returns −1 when what is sought is not present, and that value can never be mistaken for an index.

Algorithms on strings
  1. Three patterns cover almost every string problem at this level: scanning the characters one at a time and keeping a running answer; building a new string by adding characters conditionally; and taking a sentence word by word.

  2. A membership test can be written by putting the acceptable characters in a string and asking whether the character occurs in it, since a result of 0 or more means “present”.

  3. Scanning a string of \(n\) characters costs \(O(n)\), where \(n\) counts the characters. Building a string of \(n\) characters by repeated joining costs \(O(n^2)\), because each join copies everything assembled so far.

  4. A hand-written word scan must add a separator at the end so that the last word is not lost, must empty the collecting variable after each word, and must ignore empty words produced by repeated blanks.

Converting between text and numbers
  1. Parsing is reading a piece of text and working out the value it stands for. Each numeric wrapper class supplies a parsing method that returns a primitive value, while its valueOf() method returns a wrapper object instead.

  2. A number becomes text through the toString() method of its wrapper class, through String.valueOf(), or simply by being joined to a string — which is what happens in every display statement that joins a heading to a value.

  3. Parsing fails with a NumberFormatException when the text does not spell a proper number of the required kind, including when it carries a stray blank or a decimal point that a whole number cannot have. Checking the text before parsing it is part of writing a program that behaves predictably for every input.

The StringBuffer class
  1. A StringBuffer object holds characters that can be altered in place. Such an object is mutable — the opposite of a String.

  2. A buffer has both a length, the number of characters it holds, and a capacity, the number it has room for. An empty buffer is given room for 16 characters by default, and a buffer created from text is given 16 characters beyond the length of that text.

  3. Its methods alter the object they are called on rather than returning a new one, and they allow characters to be added at the end, inserted at a position, removed between two positions, replaced singly, reversed, or cut and padded to a fixed length.

  4. Building \(n\) characters in a buffer costs \(O(n)\) against \(O(n^2)\) for the same job done by joining strings, where \(n\) counts the characters in the finished text. The price of that speed is the loss of the guarantee that shared text cannot be altered behind your back.

  5. The working rule is to use a String for text that is examined and a StringBuffer for text that is assembled or edited, converting back to a String once the building is over.

The StringTokenizer class
  1. A token is a run of characters treated as one unit; a delimiter is a character that marks the boundary between tokens and is not part of either. Delimiters are discarded during splitting.

  2. A StringTokenizer, from the package java.util, breaks text into tokens. Created with the text alone it uses whitespace as its delimiters, treating a run of blanks as a single boundary; created with a second string it uses each character of that string as a delimiter.

  3. The object behaves as a queue of tokens waiting to be taken. It reports whether any remain, hands out the next one, and counts those still waiting — so the count must be taken before the tokens are consumed, and a tokenizer cannot be rewound.

  4. Splitting a sentence of \(n\) characters costs \(O(n)\), where \(n\) counts the characters. Using a tokenizer removes the three awkward details of a hand-written scan, but the characters inside a token are still reached by scanning that token.

7.10 Key Terms

  1. ASCII — American Standard Code for Information Interchange; the agreed table fixing a numeric code for each letter, digit, punctuation mark and control character, and the source of the first 128 codes of Unicode.

  2. autoboxing — the automatic conversion, performed by the compiler, of a primitive value into an object of its corresponding wrapper class.

  3. capacity — the number of characters a StringBuffer has room for before it must be enlarged, which is never less than its length.

  4. concatenation — the joining of two pieces of text end to end to form a longer one, written with the + operator or with concat().

  5. delimiter — a character that marks the boundary between one token and the next, and that forms no part of either token.

  6. immutable — describing an object whose contents cannot be altered after it has been created, so that every operation on it produces a new object; a String is immutable.

  7. length — the number of characters actually held, reported by length() for a String and for a StringBuffer.

  8. mutable — describing an object whose contents can be altered in place after it has been created; a StringBuffer is mutable.

  9. palindrome — a word or sequence of characters that reads the same from either end.

  10. parsing — reading a piece of text and working out the value it stands for, as Integer.parseInt() does for a whole number.

  11. primitive type — a type built into the language itself, a variable of which holds one plain value and carries no methods; the eight are byte, int, short, long, float, double, boolean and char.

  12. string literal — text written between double quotes in a program, which is itself a String object.

  13. substring — any run of consecutive characters taken from a string.

  14. token — a run of consecutive characters treated as one unit, such as a word, a number or a code within a larger piece of text.

  15. type cast — a conversion written by the programmer, in brackets before a value, instructing the compiler to treat that value as being of another type.

  16. unboxing — the automatic conversion, performed by the compiler, of a wrapper object into the primitive value it holds.

  17. Unicode — the character code used by Java, designed to cover the writing systems of the world, whose first 128 codes are those of ASCII.

  18. whitespace — any character that occupies space on a line without printing a mark of its own, namely the blank space, the tab and the newline.

  19. wrapper class — a class whose object holds a single value of one primitive type and supplies the methods and constants associated with that type.

7.11 Points to Ponder

  1. Immutability forces a copy every time text is changed, and the whole of section 7.7 exists to work around that cost. Why might a language designer still consider immutability worth the price? What could go wrong in a program in which any part could quietly alter a string that another part was relying on?

  2. Java could have offered only one class for text — a mutable one — and left programmers to be careful. Instead it offers two, and expects you to choose. Is a language that gives you a choice with a cost attached better than one that decides for you? Where else in programming have you met the same trade?

  3. Because capital letters were given smaller codes than small ones, "Zebra" comes before "apple" in Java’s ordering, which no dictionary would accept. Should the ordering of a language follow the codes, which are simple and predictable, or the conventions of a written language, which differ from one language to another?

  4. The searching methods answer “nowhere” by returning −1, a value that could never be a real index. Suppose the library had chosen to return 0 instead, or to stop the program with an exception. What would each of those choices have made easier, and what would it have made harder?

  5. A digit character and the number it shows are stored as different values, and converting between them takes deliberate work. Would anything be gained if a language stored '7' and 7 identically? Think about what would then happen to a program that has to decide whether the user typed a number or a word.

  6. A StringTokenizer throws its delimiters away, so a sentence split into words cannot be put back together exactly as it was. When would a program need a splitting tool that kept the separators, and what would such a tool have to hand back that this one does not?

7.12 Objective Questions

  1. A string holds the word ENVIRONMENT. What is the index of its last character?
  1. 9
  2. 10
  3. 11
  4. 12 [1 mark]
  1. Which method reports how many tokens are still waiting in a StringTokenizer object?
  1. length()
  2. size()
  3. countTokens()
  4. hasMoreTokens() [1 mark]
  1. The wrapper class corresponding to the primitive type char is:
  1. Char
  2. Chr
  3. Character
  4. CharValue [1 mark]
  1. What is the value of "Computer".substring(2, 5)?
  1. "mpu"
  2. "mput"
  3. "ompu"
  4. "put" [1 mark]
  1. Which of the following returns a primitive int rather than an object?
  1. Integer.valueOf("24")
  2. Integer.parseInt("24")
  3. Integer.toString(24)
  4. String.valueOf(24) [1 mark]
  1. Assertion (A): A String object cannot be altered after it has been created. Reason (R): Every method of the String class returns a new object and leaves the original untouched. Choose the correct option:
  1. Both (A) and (R) are true and (R) is the correct explanation of (A).
  2. Both (A) and (R) are true but (R) is not the correct explanation of (A).
    1. is true but (R) is false.
    1. is false but (R) is true. [1 mark]
  1. Assertion (A): Two strings holding the same characters may compare as unequal when tested with ==. Reason (R): The == operator tests whether two variables refer to the same object, not whether their contents match. Choose the correct option:
  1. Both (A) and (R) are true and (R) is the correct explanation of (A).
  2. Both (A) and (R) are true but (R) is not the correct explanation of (A).
    1. is true but (R) is false.
    1. is false but (R) is true. [1 mark]
  1. Match each method call in Column I with what it hands back, from Column II.
Column I Column II
a. s.charAt(2) i. a boolean
b. s.compareTo(t) ii. a String
c. s.endsWith(t) iii. a char
d. b.toString(), where b is a StringBuffer iv. an int

[1 mark]

  1. Fill in the blank: the automatic conversion of a primitive value into an object of its wrapper class is called ____________. [1 mark]

  2. State whether true or false: the trim() method removes the blank spaces occurring between the words of a string. [1 mark]

  3. Write the full form of ASCII. [1 mark]

7.13 Check Your Concepts

  1. Define a wrapper class, and name the wrapper classes corresponding to the primitive types char and int. [2 marks]

  2. What is meant by whitespace? Name the three whitespace characters used in this chapter. [2 marks]

  3. Define a token and a delimiter, giving one example of each from a record of the form Idli:30:2. [2 marks]

  4. What value does a searching method of the String class return when the character or substring sought is not present? Explain why that particular value was chosen. [2 marks]

  5. Differentiate between equals() and compareTo() on three grounds: the question each asks, the type of value each returns, and the use to which the result is put. [3 marks]

  6. Differentiate between autoboxing and unboxing, giving one statement of Java to illustrate each. [3 marks]

  7. A student writes title.toUpperCase(); and finds that the string held in title is unchanged when printed. Give reasons for this behaviour and state the correction needed. [3 marks]

  8. Explain, with a suitable example, why s.substring(b, e) returns exactly e − b characters. State what s.substring(4, 4) returns and why this is not an error. [4 marks]

  9. Give the cost, in big O notation, of building a text of n characters (i) by repeatedly joining strings and (ii) by appending to a StringBuffer. Justify each answer by saying what work is done at each step, and state clearly what n counts. [4 marks]

  10. Differentiate between String and StringBuffer under at least four heads. Then state, with reasons, which of the two you would use for (i) a student’s name that is read once and compared many times, and (ii) a report line assembled from fifty separate pieces. [6 marks]

  11. Write an algorithm, in pseudocode, that displays each word of a sentence without using the StringTokenizer class. Name the three situations your algorithm must handle correctly — the last word, repeated blanks, and blanks at the ends of the sentence — and show how each is handled. State the cost of the algorithm in big O notation, saying what n counts. [6 marks]

7.14 Output & Debugging

  1. Predict the output of the following fragment.
String s = "Rajasthan";
System.out.println(s.indexOf('a'));
System.out.println(s.lastIndexOf('a'));
System.out.println(s.substring(4, 7));

[2 marks]

  1. State what each of the three statements prints, and explain in one line why the first two differ.
String a = "40";
int b = 5;
System.out.println(a + b);
System.out.println(Integer.parseInt(a) + b);
System.out.println("Sum: " + b + b);

[3 marks]

  1. Give the output of the following fragment, and say in one line what it computes in general.
String word = "SCHOOL";
String r = "";
for (int i = 0; i < word.length(); i++)
{
    if (word.charAt(i) != 'O')
    {
        r = r + word.charAt(i);
    }
}
System.out.println(r);
System.out.println(r.length());

[3 marks]

  1. The following fragment prints seven characters and then stops with an error. Name the error, state exactly when it occurs, and rewrite the loop correctly.
String city = "Udaipur";
for (int i = 0; i <= city.length(); i++)
{
    System.out.print(city.charAt(i));
}

[3 marks]

  1. Trace the following fragment and give the two lines it prints.
StringBuffer b = new StringBuffer("PLANT");
b.setCharAt(2, 'I');
b.delete(3, 5);
b.append("CE");
System.out.println(b);
System.out.println(b.length());

[3 marks]

  1. Give the complete output of this program. The count printed on the last line is not what its author expected: state the value printed, explain why, and correct the program with the smallest change you can.
import java.util.StringTokenizer;

class Count
{
    public static void main(String args[])
    {
        StringTokenizer st = new StringTokenizer("rain in the plains");
        while (st.hasMoreTokens())
        {
            System.out.println(st.nextToken());
        }
        System.out.println("Words = " + st.countTokens());
    }
}

[4 marks]

7.15 Programming Practice

  1. Write a method int vowels(String w) that accepts a word and returns the number of vowels in it, counting capital and small letters alike. Submit the method only, together with a variable description table for the variables it uses. [5 marks]

  2. Design a class to accept a sentence from the keyboard and display it with the first letter of every word converted to a capital and the remaining letters left as typed. The words may be separated by more than one blank space. Sample input: the red fort at agra Sample output: The Red Fort At Agra Submit an algorithm, the complete class and a variable description table. [6 marks]

  3. Design a class to accept a sentence terminated by a full stop and display every word that begins and ends with the same letter, ignoring the difference between capital and small letters, followed by the number of such words. The full stop is not part of any word. Sample input: Level ground near the test area. Sample output should list the qualifying words, one to a line, and then their count. Submit an algorithm, the complete class and a variable description table. [10 marks]

  4. Design a class to accept a whole number of at most nine digits typed as text. If every character typed is a digit, display the sum of the digits and state whether the number reads the same from either end; otherwise display a message saying that the input is not a whole number. The program must not stop with an exception on any input. Sample input: 12321 Sample output should give the sum of the digits and the verdict on reading it backwards. Submit the complete class and a variable description table. [10 marks]

  5. Design a class to accept a sentence and build a single new line in which each word is followed by the number of letters in that word, written in brackets, with one blank space between the entries. The new line must be assembled in a StringBuffer and converted to a String only once, at the end. Sample input: Books open doors Sample output: Books(5) open(4) doors(5) Submit the complete class, a variable description table, and one line stating why a StringBuffer is the better choice here. [10 marks]