1. Computer Basics

Computers are useless. They can only give you answers.

— Pablo Picasso

1.1. Problem: Buying a computer

We begin almost every chapter of this book with a motivating problem. Why? Sometimes it helps to see how tools can be applied in order to see why they’re useful. As we move through each chapter, we cover background concepts needed to solve the problem in the Concepts section, the specific technical details (usually in the form of Java syntax) required in the Syntax section, and eventually the solution to the problem in the Solution sections. If you’re not interested in the problem, that’s fine! Feel free to skip ahead to Section 1.2 on hardware and software or to Section 1.3 on data representation, especially if you already have some programming experience. Then, if you’d like to see another detailed example, the solution to this problem is available in Section 1.4 as a reference.

We’ll start with a problem that’s not about programming and may be familiar to you. Imagine you’re just about to start college and need a computer. Should you buy a Mac or PC? What kind of computer is going to run programs faster? Do some kinds of computers crash more than others? Which features are worth paying more for? Why are there so many buzzwords and so much impenetrable jargon associated with buying a computer?

When many people hear “computer science,” these are often the first questions that come to mind. Most of this book is about programming computers in a language called Java and not about the computers themselves. We try to present all material so that almost any kind of computer can be used when programming the problems and examples. Nevertheless, both the hardware that makes up your computer and the other software running on it affect the way the programs you write work.

1.2. Concepts: Hardware and software

Computers are ubiquitous. We see them nearly everywhere. They are found in most homes, shops, cars, aircraft, phones, and inside many other devices. Sometimes they are obvious, like a laptop sitting on a desk, but most computers are hidden inside other devices such as a watch or a flat-panel television. Computers can be complex or relatively simple machines. Despite their diversity, we can think of all computers in terms of their hardware and the software that runs on it.

1.2.1. Hardware

Hardware consists of the physical components that make up a computer but not the programs or data stored on it. Hardware components can be seen and touched, if you are willing to open the computer case. One way to organize hardware is to break it down into three categories: the processor, the memory, and input and output (I/O) devices.

This view of a computer is a simplified version of what is called the von Neumann architecture or a stored-program computer. It’s a good (but imperfect) model of most modern computers. In this model, a program (a list of instructions) is stored in memory. The processor loads the program and performs the instructions, some of which require the processor to do a lot of number crunching. Sometimes the processor reads data out of memory or writes new data into it. Periodically, the processor may send output to one of the output devices or receive input from one of the input devices.

In other words, the processor thinks, the memory stores, and the I/O devices talk to the outside world. The processor sits between the memory and the I/O devices. Let’s examine these categories further.

vonNeumann
Figure 1.1 Hardware components in a typical desktop computer categorized into CPU, memory, and I/O devices.
CPU

The processor, or central processing unit (CPU), is the “brain” of a computer. It fetches instructions, decodes them, and executes them. It may send data to or from memory or I/O devices. The CPU on virtually all modern computers is a microprocessor, meaning that all the computation is done by an integrated circuit fabricated out of silicon. What are the important features of CPUs? How do we measure their speed and power?

Frequency

The speed of a CPU (and indeed a computer as a whole) is often quoted in gigahertz (GHz). Hertz (Hz) is a measurement of frequency. If something happens once per second, it has a frequency of exactly 1 Hz. Perhaps the second hand on your watch moves with a frequency of 1 Hz. In North America, the current in electrical outlets alternates with a frequency of approximately 60 Hz. Sound can also be measured by frequency. The lowest-pitched sound the human ear can hear is around 20 Hz. The highest-pitched sound is around 20,000 Hz. Such a sound pulses against your eardrum 20,000 times per second. That sounds like a lot, but many modern computers operate at a frequency of 1 to 4 gigahertz. The prefix “giga” means “billion.” So, we’re talking about computers doing something more than a billion (1,000,000,000) times per second.

But what are they doing? This frequency is the clock rate, which marks how often a regular electrical signal passes through the CPU. On each tick, the CPU does some computation. How much? It depends. On some systems, simple instructions (like adding two numbers) can be computed in a single clock cycle. Other instructions can take ten or more clock cycles. Different processor designs can take different numbers of cycles to execute the same instructions. Instructions are also pipelined, meaning that one instruction is being executed while another one is being fetched from memory or decoded. Different processors can have different ways of optimizing this process. Because of these differences, the frequency of a processor as measured in gigahertz is not an accurate way to compare the effective speed of one processor to another, unless the two processors are very closely related. Even though it doesn’t really make sense, clock rate is commonly advertised as the speed of a computer.

Word size

Perhaps you have heard of a 32-bit or 64-bit computer. As we discuss in the subsection about memory, a bit is a 0 or a 1, the smallest amount of information you can record. Most new laptop and desktop computers are 64-bit machines, meaning that they operate on 64 bits at a time and can use 64-bit values as memory addresses. The instructions that they execute often perform calculations on 64-bit quantities, i.e., numbers made up of 64 0s and 1s. The size of data that a computer can operate on with a single instruction is known as its word size.

In day-to-day operations, word size is not important to most users. Certain programs that interact directly with the hardware, such as the operating system, may be affected by the word size. For example, most modern 32-bit operating systems are designed to run on a 64-bit processor, but most 64-bit operating systems do not run on a 32-bit processor.

Programs often run faster on machines with a larger word size, but they typically take up more memory. A 32-bit processor (or operating system) cannot use more than 4 gigabytes (defined below) of memory. Thus, a 64-bit computer is needed to take advantage of the larger amounts of memory that are now available.

Cache

Human brains both perform computations and store information. A computer CPU performs computations, but for the most part, does not store information. The CPU cache is the exception. Most modern CPUs have a small, very fast section of memory built right onto the chip. By guessing what information the CPU is going to use next, it can pre-load it into the cache and avoid waiting around for the slower regular memory.

Over time, caches have become more complicated and often have multiple levels. The first level is very small but incredibly fast. The second level is larger and slower. And so on. It would be preferable to have a large, first-level cache, but fast memory is expensive memory. Each level is larger, slower, and cheaper than the last.

Cache size is not a heavily advertised CPU feature, but it makes a huge difference in performance. A processor with a larger cache can often outperform a processor that’s faster in terms of clock rate.

Cores

Most laptops and desktops available today have multicore processors. These processors contain two, four, six, or even more cores. Each core is a processor capable of independently executing instructions, and they can all communicate with the same memory.

In theory, having six cores could allow your computer to run six times as fast. In practice, this speedup is hard to achieve. Learning how to get more performance out of multicore systems is a major themes of this book. Chapter 13 and Chapter 14 as well as sections marked Concurrency in other chapters are specifically tailored for students interested in programming these multicore systems to work effectively. If you aren’t interested in concurrent programming, you can skip these chapters and sections and use this book as a traditional introductory Java programming textbook. On the other hand, if you are interested in the increasingly important area of concurrent programming, Section 1.4.1 near the end of this chapter is the first Concurrency section of the book and discusses multicore processors more deeply.

Memory

Memory is where all the programs and data on a computer are stored. The memory in a computer is usually not a single piece of hardware. Instead, the storage requirements of a computer are met by many different technologies.

At the top of the pyramid of memory is primary storage, memory that the CPU can access and control directly. On desktop and laptop computers, primary storage usually takes the form of random access memory (RAM). It is called random access memory because it takes the same amount of time to access any part of RAM. Traditional RAM is volatile, meaning that its contents are lost when it’s unpowered. All programs and data must be loaded into RAM to be used by the CPU.

After primary storage comes secondary storage. The realm of secondary storage is dominated by hard drives that store data on spinning magnetic platters though flash technology is beginning to replace them. Optical drives (such as CD, DVD, and Blu-ray) and the now virtually obsolete floppy drives also fall into the category of secondary storage. Secondary storage is slower than primary storage, but it is non-volatile. Some forms of secondary storage such as CD-ROM and DVD-ROM are read only, but most are capable of reading and writing.

Before we can compare these kinds of storage effectively, we need to have a system for measuring how much they store. In modern digital computers, all data is stored as a sequence of 0s and 1s. In memory, the space that can hold either a single 0 or a single 1 is called a bit, which is short for “binary digit.”

A bit is a tiny amount of information. For organizational purposes, we call a sequence of eight bits a byte. The word size of a CPU is two or more bytes, but memory capacity is usually listed in bytes not words.

bitsBytesFigure
Figure 1.2 Shown here is a word containing four bytes, or 32 bits. Computer scientists often number items starting at zero, as we discuss in Chapter 6.

Both primary and secondary storage capacities have become so large that it is inconvenient to describe them in bytes. Computer scientists have borrowed prefixes from physical scientists to create suitable units.

Common units for measuring memory are bytes, kilobytes, megabytes, gigabytes, and terabytes. Each unit is 1,024 times the size of the previous unit. You may have noticed that 210 (1,024) is almost the same as 103 (1,000). Sometimes it’s not clear which value is meant. Disk drive manufacturers always use powers of 10 when they quote the size of their disks. Thus, a 1 TB hard disk might hold 1012 (1,000,000,000,000) bytes, not 240 (1,099,511,627,776) bytes. Standards organizations have advocated that the terms kibibyte (KiB), mebibyte (MiB), gibibyte (GiB), and tebibyte (TiB) be used to refer to the units based on powers of 2 while the traditional names be used to refer only to the units based on powers of 10, but the new terms have not yet become popular.

Unit Size Bytes Practical Measure

byte

8 bits

20 = 100

a single character

kilobyte (KB)

1,024 bytes

210 ≈ 103

a paragraph of text

megabyte (MB)

1,024 kilobytes

220 ≈ 106

a minute of MP3 music

gigabyte (GB)

1,024 megabytes

230 ≈ 109

an hour of standard definition streaming video

terabyte (TB)

1,024 gigabytes

240 ≈ 1012

80% of human memory capacity,
estimated by Raymond Kurzweil

We called memory a pyramid earlier in this section. At the top there’s a small but very fast amount of memory. As we work down the pyramid, the storage capacity grows, but the speed slows down. Of course, the pyramid for every computer is different. Below is a table that shows many kinds of memory moving from the fastest and smallest to the slowest and largest. Effective speed is hard to measure (and is changing as technology progresses), but note that each layer in the pyramid tends to be 10-100 times slower than the previous layer.

Memory Typical Capacity Use

Cache

kilobytes or megabytes

Cache is fast, temporary storage for the CPU itself. Modern CPUs have two or three levels of cache that get progressively bigger and slower.

RAM

gigabytes

The bulk of primary memory is RAM. RAM comes on sticks that can be swapped out to upgrade a computer.

Flash drives

gigabytes up to terabytes

Flash drives provide some of the fastest secondary storage available to regular consumers. Flash drives come as USB keychain drives but also as drives that sit inside the computer (sometimes called solid state drives or SSDs). As the price of flash drives drops, they are expected to replace hard drives entirely. Some SSDs already have capacities in the terabyte range.

Hard drives

terabytes

Hard drives are still the most common secondary storage for desktops, laptops, and servers. They are limited in speed partly because of their moving parts.

Tape backup

terabytes and beyond

Some large companies still store huge quantities of information on magnetic tape. Tape performs well for long sequential accesses.

Network storage

terabytes and beyond

Storage that is accessed through a network is limited by the speed of the network. Many companies use networked computers for backup and redundancy as well as distributed computation. Amazon, Google, Microsoft, and others rent their network storage systems at rates based on storage size and total data throughput. These services are part of what is called cloud computing.

I/O devices

I/O devices have much more variety than CPUs or memory. Some I/O devices, such as USB ports, are permanently connected by a printed circuit board to the CPU. Other devices called peripherals are connected to a computer as needed. Their types and features are many and varied, and this book does not go deeply into how to interact with I/O devices.

Common input devices include mice, keyboards, touch pads, microphones, game pads, and drawing tablets. Common output devices include monitors, speakers, and printers. Some devices perform both input and output, such as network cards.

Remember that our view of computer hardware as CPU, memory, and I/O devices is only a model. A PCI Express socket can be considered an I/O device, but the graphics card that fits into the socket can be considered one as well. And the monitor that connects to the graphics card is yet another one. Although the graphics card is an I/O device, it has its own processor and memory, too. It’s pointless to get bogged down in details unless they are relevant to the problem you’re trying to solve. One of the most important skills in computer science is finding the right level of detail and abstraction to view a given problem.

1.2.2. Software

Without hardware computers would not exist, but software is equally important. Software consists of the programs and data that are executed and stored by the computer. The focus of this book is learning to write software.

Software includes the nearly infinite variety of computer programs. With the right tools (many of which are free), anyone can write a program that runs on a Windows, Mac, or Linux machine. Although it would be nearly impossible to list all the different kinds of software, a few categories are worth mentioning.

Operating Systems

The operating system (OS) is the software that manages the interaction between the hardware and the rest of the software. Programs called drivers are added to the OS for each hardware device. For example, when an application wants to print a document, it communicates with the printer via a printer driver that’s customized for the specific printer, the OS, and the computer hardware. The OS also schedules, runs, and manages memory for all other programs. The three most common OSes for desktop machines are Microsoft Windows, Apple macOS, and Linux. At the present time, all three run on similar hardware based on the Intel x86 and x64 architectures.

Microsoft does not sell desktop computers, but many desktop and laptop computers come bundled with Windows. For individuals and businesses who assemble their own computer hardware, it’s also possible to purchase Windows separately. In contrast, almost all computers running macOS are sold by Apple, and macOS is usually bundled with the computer. Linux is open-source software, meaning that all the source code used to create it is freely available. In spite of Linux being free, many consumers prefer Windows or macOS because of ease of use, compatibility with specific software, and technical support. Many consumers are also unaware that hardware can be purchased separately from an OS or that Linux is a free alternative to the other two.

Other computers have OSes as well. Many kinds of mobile telephones use the Google Android OS. The Apple iPad and iPhone use the competing Apple iOS. Phones, microwave ovens, automobiles, and countless other devices have computers in them that use some kind of embedded OS.

Consider two applications running on a mobile phone with a single core CPU. One application is a web browser and the other is a music player. The user may start listening to music and then start the browser. In order to function, both applications need to access the CPU at the same time. Since the CPU only has a single core, it can execute only one instruction at a time.

Rather than forcing the user to finish listening to the song before using the web browser, the OS switches the CPU between the two applications very quickly. This switching allows the user to continue browsing while the music plays in the background. The user perceives an illusion that both applications are using the CPU at the same time.

Compilers

A compiler is a kind of program that’s particularly important to programmers. Computer programs are written in special languages, such as Java, that are human readable. A compiler takes this human-readable program and turns it into instructions (often machine code) that a computer can understand.

To compile the programs in this book, you use the Java compiler javac, either directly by typing its name as a command or indirectly as Eclipse, IntelliJ IDEA, or some other tool that runs the compiler for you.

Business Applications

Many different kinds of programs fall under the umbrella of business or productivity software. Perhaps the best known is the Microsoft Office suite of tools, which includes the word-processing software Word, the spreadsheet software Excel, and the presentation software PowerPoint.

Programs in this category are often the first to come to mind when people think of software, and this category has had tremendous historical impact. The popularity of Microsoft Office led to the widespread adoption of Microsoft Windows in the 1990s. A single application that’s so desirable that a consumer is willing to buy the hardware and the OS just to be able to run it is sometimes called a killer app.

Video Games

Video games are software like other programs, but they deserve special attention because they represent an enormous, multi-billion dollar industry. They are usually challenging to program, and the video game development industry is highly competitive.

The intense 3D graphics required by modern video games have pushed hardware manufacturers such as Nvidia, AMD, and Intel to develop high-performance graphics cards for desktop and laptop computers. At the same time, companies like Nintendo, Sony, and Microsoft have developed computers such as the Switch, PlayStation 4, and Xbox One that specialize in video games but are not designed for general computing tasks.

Web Browsers

Web browsers are programs that can connect to the Internet and download and display web pages and other files. Early web browsers could only display relatively simple pages containing text and images. Because of the growing importance of communication over the Internet, web browsers have evolved to play sounds, display video, and allow for sophisticated real-time communication.

Popular web browsers include Microsoft Edge, Mozilla Firefox, Apple Safari, and Google Chrome. Each has advantages and disadvantages in terms of compatibility, standards compliance, security, speed, and customer support. The Opera web browser is not well known on desktop computers, but it is used on many mobile telephones.

1.3. Syntax: Data representation

After each Concepts section, this book usually has a Syntax section. Syntax is the set of rules for a language. These Syntax sections generally focus on concrete Java language features and technical specifics related to the concepts described in the chapter.

In this chapter, we’re still trying to describe computers at a general level. Consequently, the technical details we cover in this section will not be Java syntax. Although everything we say applies to Java, it also applies to many other programming languages.

1.3.1. Compilers and interpreters

This book is primarily about solving problems with computer programs. From now on, we only mention hardware when it has an impact on programming. The first step to writing a computer program is deciding what language to use.

Most humans communicate via natural languages such as Chinese, English, French, Russian, or Tamil. However, computers are poor at understanding natural languages. As a compromise, programmers write programs (instructions for a computer to follow) in a language more similar to a natural language than it is to the language understood by the CPU. These languages are called high-level languages, because they are closer to natural language (the highest level) than they are to machine language (the lowest level). We may also refer to machine language as machine code or native code.

Thousands of programming languages have been created over the years, but some of the most popular high-level languages of all time include Fortran, Cobol, Visual Basic, C, C++, Python, Java, JavaScript (which is almost entirely unrelated to Java) and C#.

As we mentioned in the previous section, a compiler is a program that translates one language into another. In many cases, a compiler translates a high-level language into a low-level language that the CPU can understand and execute. Because all the work is done ahead of time, this kind of compilation is known as static or ahead-of-time compilation. In other cases, the output of the compiler is an intermediate language that’s easier for the computer to understand than the high-level language but still takes some translation before the computer can follow the instructions.

An interpreter is a program with many similarities to a compiler. However, an interpreter takes code in one language as input and, on the fly, runs each instruction on the CPU as it translates it. Interpreters generally execute code more slowly than if it had been translated to machine language before execution.

Note that both compilers and interpreters are normal programs. They are usually written in high-level languages and compiled into machine language before execution. This raises a philosophical question: If you need a compiler to create a program, where did the first compiler come from?

compilerFigure
Figure 1.3 (a) Static compilation. (b) Interpreted execution. (c) Compilation into bytecode with later just-in-time compilation.
Example 1.1 Java compilation

Java is the popular high-level programming language we focus on in this book. The standard way to run a Java program has an extra step that many compiled languages do not. Most compilers for Java, though not all, translate a program written in Java to an intermediate language known as bytecode. This intermediate version of the high-level program is used as input for another program called the Java Virtual Machine (JVM). Most popular JVMs translate the bytecode into machine code that is executed directly by the CPU. This conversion from bytecode into machine code is done with a just-in-time (JIT) compiler. It’s called “just-in-time” because sections of bytecode are not compiled until the moment they’re needed. Since the output is going to be used for this specific execution of the program, the JIT can do optimizations to make the final machine code run particularly well in the current environment.

Why does Java use the intermediate step of bytecode? One of Java’s design goals is to be platform independent, meaning that it can be executed on any kind of computer. This is a difficult goal because every combination of OS and CPU will need different low-level instructions. Java attacks the problem by keeping its bytecode platform independent. You can compile a program into bytecode on a Windows machine and then run the bytecode on a JVM in a macOS environment. Part of the work is platform independent, and part is not. Each JVM must be tailored to the combination of OS and hardware that it runs on. Sun Microsystems, Inc., the original developer of the Java language and the JVM, marketed this feature of the language with the slogan “Write once, run anywhere.”

Sun Microsystems was bought by Oracle Corporation in 2009. Oracle continues to produce HotSpot, the standard JVM, but many other JVMs exist, including Apache Harmony and Dalvik, the Google Android JVM.

1.3.2. Numbers

All data inside of a computer is represented with numbers. Although humans use numbers in our daily lives, the representation and manipulation of numbers by computers function differently. In this subsection we introduce the notions of number systems, bases, conversion from one base to another, and arithmetic in arbitrary number systems.

A few number systems

A number system is a way to represent numbers. It’s easy to confuse the numeral that represents the number with the number itself. You might think of the number ten as “10”, a numeral made of two symbols, but the number itself is the concept of ten-ness. You could express that quantity by holding up all your fingers, with the symbol “X”, or by knocking ten times.

Representing ten with “10” is an example of a positional number system, namely base 10. In a positional number system, the position of the digits determines the magnitude they represent. For example, the numeral 3,432 contains the digit 3 twice. The first time, it represents three groups of one thousand. The second time, it represents three groups of ten. In contrast, the Roman numeral system is an example of a number system that is not positional.

The numeral 3,432 and possibly every other normally written number you’ve seen is expressed in base 10 or the decimal system. It’s called base 10 because, as you move from the rightmost digit leftward, the value of each position goes up by a factor of 10. Also, in base 10, ten is the smallest positive integer that requires two digits for representation. Each smaller number has its own digit: 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9. Representing ten requires two existing digits to be combined. Every base has the property that the number it’s named after takes two digits to write, namely “1” and “0.” (An exception is base 1, which does not behave like the other bases and is not a normal positional number system.)

Example 1.2 Decimal numbers

The number 723 can be written as 723 = 7 â‹… 102 + 2 â‹… 101 + 3 â‹… 100.

Note that the rightmost digit is the ones place, which is equivalent to 100. Be sure to start with b0 and not b1 when considering the value of a number written in base b, no matter what b is. The second digit from the right is multiplied by 101, and so on. The product of a digit and the corresponding power of 10 tells us how much a digit contributes to the number. In the above expansion, digit 7 contributes 700 to the number 723. Digits 2 and 3 contribute, respectively, 20 and 3 to 723.

As we move to the right, the power of 10 goes down by one, and this pattern works even for negative powers of 10. If we expand the fractional value 0.324, we get 0.324 = 3 â‹… 10-1 + 2 â‹… 10-2 + 4 â‹… 10-3.

We can combine the above two numbers to get 723.324 = 7 â‹… 102 + 2 â‹… 101 + 3 â‹… 100 + 3 â‹… 10-1 + 2 â‹… 10-2 + 4 â‹… 10-3.

We can extend these ideas to any base, checking our logic against the familiar base 10. Suppose that a numeral consists of n symbols sn-1, sn-2, …, s1, s0. Furthermore, suppose that this numeral belongs to the base b number system. We can expand the value of this numeral as:

sn-1 sn-2 … s1 s0 = sn-1 ⋅ bn-1 + sn-2 ⋅ bn-2 + … + s1 ⋅ b1 + s0 ⋅ b0

The leftmost symbol in the numeral is the highest order digit and the rightmost symbol is the lowest order digit. For example, in the decimal numeral 492, 4 is the highest order digit and 2 the lowest order digit.

Fractions can be expanded in a similar manner. For example, a fraction with n symbols s1, s2, …, sn-1, sn in a number system with base b can be expanded to:

0.s1 s2 … sn-2 sn-1 = s1 ⋅ b-1 + s2 ⋅ b-2 + … + sn-1 ⋅ b-n+1 + sn ⋅ b-n

As computer scientists, we have a special interest in base 2 because that’s the base used to express numbers inside of computers. Base 2 is also called binary. The only symbols allowed to represent numbers in binary are “0” and “1”, the binary digits or bits.

In the binary numeral 10011, the leftmost 1 is the highest order bit and the rightmost 0 is the lowest order bit. By the rules of positional number systems, the highest order bit represents 1 â‹… 24 = 16.

Example 1.3 Binary numbers

Examples of numbers written in binary are 1002, 1112, 101012, and 110000012. Recall that the base of the binary number system is 2. Thus, we can write a number in binary as the sum of products of powers of 2. For example, the numeral 100112 can be expanded to:

100112 = 1 â‹… 24 + 0 â‹… 23 + 0 â‹… 22 + 1 â‹… 21 + 1 â‹… 20 = 16 + 0 + 0 + 2 + 1 = 19

By expanding the number, we’ve also shown how to convert a binary numeral into a decimal numeral. Remember that both 100112 and 19 represent the same value, namely nineteen. The conversion between bases changes only the way the number is written. As before, the rightmost bit is multiplied by 20 to determine its contribution to the binary number. The bit to its left is multiplied by 21 to determine its contribution, and so on. In this case, the leftmost 1 contributes 1 â‹… 24 = 16 to the value.

Another useful number system is base 16, also known as hexadecimal. Hexadecimal is surprising because it requires more than the familiar 10 digits. Numerals in this system are written with 16 hexadecimal digits that include the ten digits 0 through 9 and the six letters A, B, C, D, E, and F. The six letters, starting from A, correspond to the values 10, 11, 12, 13, 14, and 15.

Hexadecimal is used as a compact representation of binary. Although binary numbers can get very long, four binary digits can be represented with only a single hexadecimal digit.

Example 1.4 Hexadecimal numbers

39A16, 3216, and AFBC1216 are examples of numbers written in hexadecimal. A hexadecimal numeral can be expressed as the sum of products of powers of 16. For example, the hexadecimal numeral A0BF16 can be expanded to:

A â‹… 163 + 0 â‹… 162 + B â‹… 161 + F â‹… 160

To convert a hexadecimal numeral to decimal, we must substitute the values 10 through 15 for the digits A through F. Now we can rewrite the sum of products from above as:

10 â‹… 163 + 0 â‹… 162 + 11 â‹… 161 + 15 â‹… 160 = 40,960 + 0 + 176 + 15 = 41,151

Thus, we get A0BF16 = 41,151.

The base 8 number system is also called octal. Like hexadecimal, octal is used as a shorthand for binary. A numeral in octal uses the octal digits 0, 1, 2, 3, 4, 5, 6, and 7. Otherwise the same rules apply. For example, the octal numeral 377 can be expanded to:

3778 = 3 â‹… 82 + 7 â‹… 81 + 7 â‹… 80 = 255

You may have noticed that it is not always clear which base a numeral is written in. The digit sequence 337 is a legal numeral in octal, decimal, and hexadecimal, but it represents different numbers in each system. Mathematicians use a subscript to denote the base in which a numeral is written.

Thus, 3378 = 25510, 37710 = 37710, and 37716 = 88710. Base numbers are always written in base 10. A number without a subscript is assumed to be in base 10. In Java, there’s no way to mark subscripts, so prefixes are used. A prefix of 0 is used for octal, no prefix is used for decimal, and a prefix of 0x is used for hexadecimal. A numeral cannot be marked as binary in Java. The corresponding numerals in Java code would thus be written 0377, 377, and 0x377. Be careful not to pad numbers with zeroes in Java since they might be interpreted as base 8! Remember that the value 056 is not the same as the value 56 in Java.

The following table lists a few characteristics of the four number systems we have discussed with representations of the numbers 7 and 29.

Number System Base Digits Math
Numerals
Java
Numerals

Binary

2

0, 1

1112, 111012

N/A

Octal

8

0, 1, 2, 3, 4, 5, 6, 7

78, 358

07, 035

Decimal

10

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

7, 29

7, 29

Hexadecimal

16

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F

716, 1D16

0x7,0x1D

Conversion across number systems

It’s useful to know how to convert a number represented in one base to the equivalent representation in another base. Our examples have shown how to convert a numeral in any base to decimal by expanding the numeral in the sum-of-product form and then adding the different terms together. But how do you convert a decimal numeral to another base?

Decimal to binary conversion

There are at least two different ways to convert a decimal numeral to binary. One way is to write the decimal number as a sum of powers of two as in the following conversion of the number 23.

23 = 16 + 0 + 4 + 2 + 1 = 1 â‹… 24 + 0 â‹… 23 + 1 â‹… 22 + 1 â‹… 21 + 1 â‹… 20 = 101112

First, find the largest power of two that’s less than or equal to the number. In this case, 16 fits the bill because 32 is too large. Subtract that value from the number, leaving 7 in this case. Then repeat the process. The last step is to collect the coefficients of the powers of two into a sequence to get the binary equivalent. We used 16, 4, 2, and 1 but skipped 8. If we write a 1 for every place we used and a 0 for every place we skipped, we get 23 = 101112. While this is a straightforward procedure for decimal to binary conversion, it can be cumbersome for larger numbers.

An alternate way to convert a decimal numeral to an equivalent binary numeral is to divide the given number by 2 until the quotient is 0 (keeping only the integer part of the quotient). At each step, record the remainder found when dividing by 2. Collect these remainders (which will always be either 0 or 1) to form the binary equivalent. The least significant bit is the remainder obtained after the first division, and the most significant bit is the remainder obtained after the last division. In other words, this approach finds the digits of the binary number in backward order.

Example 1.5 Decimal to binary with remainders

Let’s use this method to convert 23 to its binary equivalent. The following table shows the steps need for the conversion. The leftmost column lists the step number. The second column contains the number to be divided by 2 at each step. The third column contains the quotient for each step, and the last column contains the current remainder.

Step Number Quotient Remainder

1

23

11

1

2

11

5

1

3

5

2

1

4

2

1

0

5

1

0

1

We begin by dividing 23 by 2, yielding 11 as the quotient and 1 as the remainder. The quotient 11 is then divided by 2, yielding 5 as the quotient and 1 as the remainder. This process continues until we get a quotient of 0 and a remainder of 1 in Step 5. We now write the remainders from the most recent to the least recent and get the same result as before, 23 = 101112.

Other conversions

A decimal number can be converted to its hexadecimal equivalent using either of the two procedures described above. Instead of writing a decimal number as a sum of powers of 2, one writes it as a sum of powers of 16. Similarly, when using the division method, instead of dividing by 2, one divides by 16. Octal conversion is similar.

We use hexadecimal because it’s straightforward to convert from it to binary or back. The following table lists binary equivalents for the 16 hexadecimal digits.

Hexadecimal
Digit
Binary
Equivalent
Hexadecimal
Digit
Binary
Equivalent

0

0000

8

1000

1

0001

9

1001

2

0010

A

1010

3

0011

B

1011

4

0100

C

1100

5

0101

D

1101

6

0110

E

1110

7

0111

F

1111

With the help of the table above, let’s convert 3FA16 to binary. By simple substitution, 3FA16 = 0011 1111 10102. Note that we’ve grouped the binary digits into clusters of 4 bits each. Of course, the leftmost zeroes in the binary equivalent are useless as they do not contribute to the value of the number.

Integer representation in a computer

In mathematics, binary numerals can represent arbitrarily big numbers. Inside of a computer, the size of a number is constrained by the number of bits used to represent it. For general purpose computation, 32- and 64-bit integers are commonly used. The largest integer that Java represents with 32 bits is 2,147,483,647, which is good enough for most tasks. For larger numbers, Java can represent up to 9,223,372,036,854,775,807 with 64 bits. Java also provides representations for integers using 8 and 16 bits.

These representations are easy to determine for positive numbers: Find the binary equivalent of the number and then pad the left side with zeroes to fill the remaining space. For example, 19 = 100112. If stored using 8 bits, 19 would be represented as 0001 0011. If stored using 16 bits, 19 would be represented as 0000 0000 0001 0011. (We separate groups of 4 bits for easier reading.)

Binary arithmetic

Recall that computers deal with numbers in their binary representation, meaning that all arithmetic is done on binary numbers. Sometimes it’s useful to understand how this process works and compare it to decimal arithmetic. The table below lists rules for binary addition.

+

0

1

0

0

1

1

1

10

As indicated above, the addition of two 1s leads to a 0 with a carry of 1 into the next position to the left. Addition for numbers composed of more than one bit use the same rules as any addition, carrying values that are too large into the next position. In decimal addition, values over 9 must to be carried. In binary addition, values over 1 must be carried. The next example shows a sample binary addition. To simplify its presentation, we assume that the integers are represented with only 8 bits.

Example 1.6 Binary addition

Let’s add the numbers 60 and 6 in binary. Using the conversion techniques described above, we can find that 60 = 1111002 and 6 = 1102. Inside the computer, these numbers would already be in binary and padded to fill 8 bits.

Binary Decimal

0011 1100

60

+

0000 0110

6

0100 0010

66

The result is no surprise, but note that the addition can proceed in binary without conversion to decimal at any point.

Subtraction in binary is also similar to subtraction in decimal. The rules are given in the following table.

-

0

1

0

0

1

1

(1)1

0

When subtracting a 1 from a 0, a 1 is borrowed from the next left position. The next example illustrates binary subtraction.

Example 1.7 Binary subtraction

Again, we’ll use 60 and 6 and their binary equivalents given above.

Binary Decimal

0011 1100

60

-

0000 0110

6

0011 0110

54

Negative integers in a computer

Negative integers are also represented in computer memory as binary numbers, using a system called two’s complement. When looking at the binary representation of a signed integer in a computer, the leftmost (most significant) bit will be 1 if the number’s negative and 0 if it’s positive. Unfortunately, there’s more to finding the representation of a negative number than flipping this bit.

Suppose that we need to find the binary equivalent of the decimal number -12 using 8 bits in two’s complement form. The first step is to convert 12 to its 8-bit binary equivalent. Doing so we get 12 = 0000 1100. Note that the leftmost bit of the representation is a 0, indicating that the number is positive. Next, we take the two’s complement of the 8-bit representation in two steps. In the first step, we flip every bit, i.e., change every 0 to 1 and every 1 to 0. This gives us the one’s complement of the number, 1111 0011. In the second step, we add 1 to the one’s complement to get the two’s complement. The result is 1111 0011 + 1 = 1111 0100.

Thus, the 8-bit, two’s complement binary equivalent of -12 is 1111 0100. Note that the leftmost bit is a 1, indicating that this is a negative number.

Example 1.8 Decimal to two’s complement

Let’s convert -29 to its binary equivalent assuming that the number will be stored in 8-bit, two’s complement form. First we convert positive 29 to its 8-bit binary equivalent, 29 = 0001 1101.

Next we obtain the one’s complement of the binary representation by flipping 0s to 1s and 1s to 0s. This gives us 1110 0010. Finally, we add 1 to the one’s complement representation to get 1110 0010 + 1 = 1110 0011, which is the desired binary equivalent of -29.

Example 1.9 Two’s complement to decimal

Let’s convert the 8-bit, two’s complement value 1000 1100 to decimal. We note that the leftmost bit of this number is 1, making it a negative number. Therefore, we reverse the process of making a two’s complement. First, we subtract 1 from the representation, yielding 1000 1100 - 1 = 1000 1011. Next, we flip all the bits in this one’s complement form, yielding 0111 0100.

We convert this binary representation to its decimal equivalent, yielding 116. Thus, the decimal equivalent of 1000 1100 is -116.

Why do computers use two’s complement? First of all, they need a system that can represent both positive and negative numbers. They could have used the leftmost bit as a sign bit and represented the rest of the number as a positive binary number. Doing so would require a check on the bit and some conversion for negative numbers every time a computer wanted to perform an addition or subtraction.

Because of the way it’s designed, positive and negative integers stored in two’s complement can be added or subtracted without any special conversions. The leftmost bit is added or subtracted just like any other bit, and values that carry past the leftmost bit are ignored. Two’s complement has an advantage over one’s complement in that there is only one representation for zero. The next example shows two’s complement in action.

Example 1.10 Two’s complement arithmetic

We’ll add -126 and 126. After performing the needed conversions, their 8-bit, two’s complement representations are 1000 0010 and 0111 1110.

Binary Decimal

1000 0010

-126

+

0111 1110

126

0000 0000

0

As expected, the sum is 0.

Now, let’s add the two negative integers -126 and -2, whose 8-bit, two’s complement representations are 1000 0010 and 1111 1110.

Binary Decimal

1000 0010

-126

+

1111 1110

-2

1000 0000

-128

The result is -128, which is the smallest negative integer that can be represented in 8-bit two’s complement.

Overflow and underflow

When performing arithmetic on numbers, an overflow is said to occur when the result of the operation is larger than the largest value that can be stored in that representation. An underflow is said to occur when the result of the operation is smaller than the smallest possible value.

Both overflows and underflows lead to wrapped-around values. For example, adding two positive numbers together can result in a negative number or adding two negative numbers together can result in a positive number.

Example 1.11 Binary addition with overflow

Let’s add the numbers 124 and 6. Their 8-bit, two’s complement representations are 0111 1100 and 0000 0110.

Binary

Decimal

0111 1100

124

+

0000 0110

6

1000 0010

-126

This surprising result happens because the largest 8-bit two’s complement integer is 127. Adding 124 and 6 yields 130, a value larger than this maximum, resulting in overflow with a negative answer.

The smallest (most negative) number that can be represented in 8-bit two’s complement is -128. A result smaller than this will result in underflow. For example, -115 - 31 = 110. Try out the conversions needed to test this result.

Bitwise operators

Although we most commonly manipulate numbers using traditional mathematical operations such as addition, subtraction, multiplication, and division, there are also operations that work directly on the binary representations of the numbers. Some of these operators have clear relationships to mathematical operations, and some don’t.

Operator Name Description

&

Bitwise AND

Combines two binary representations into a new representation which has a 1 in every position where both the original representations have a 1

|

Bitwise OR

Combines two binary representations into a new representation which has a 1 in every position where either of the original representations has a 1

^

Bitwise XOR

Combines two binary representations into a new representation which has a 1 in every position that the original representations have different values

~

Bitwise complement

Takes a representation and creates a new representation in which every bit is flipped from 0 to 1 and 1 to 0

<<

Signed left shift

Moves all the bits the specified number of positions to the left, shifting 0s into the rightmost bits

>>

Signed right shift

Moves all the bits the specified number of positions to the right, padding the left with copies of the sign bit

>>>

Unsigned right shift

Moves all the bits the specified number of positions to the right, padding with 0s

Bitwise AND, bitwise OR, and bitwise XOR take two integer representations and combine them to make a new representation. In bitwise AND, each bit in the result will be a 1 if both of the original integer representations in that position are 1 and 0 otherwise. In bitwise OR, each bit in the result will be a 1 if either of the original integer representations in that position are 1 and 0 otherwise. In bitwise XOR, each bit in the result will be a 1 if the two bits of the original integer representations in that position are different and 0 otherwise.

Bitwise complement is a unary operator like the negation operator (-). Instead of merely changing the sign of a value (which it will also do), its result changes every 1 in the original representation to 0 and every 0 to 1.

The signed left shift, signed right shift, and unsigned right shift operators all create a new binary representation by shifting the bits in the original representation a certain number of places to the left or the right. The signed left shift moves the bits to the left, padding with 0s. If you do a signed left shift by n positions, it’s equivalent to multiplying the number by 2n (until overflow occurs). The signed right shift moves the bits to the right, padding with whatever the sign bit is. If you do a signed right shift by n positions, it’s equivalent to dividing the number by 2n (with integer division). The unsigned right shift moves the bits to the right, including the sign bit, filling the left side with 0s. An unsigned right shift will always make a value positive but is otherwise similar to a signed right shift. A few examples follow.

Example 1.12 Bitwise operators

Here are a few examples of the result of bitwise operations. We’ll assume that the values are represented using 32-bit two’s complement, instead of using 8-bit values as before. In Java, bitwise operators automatically convert smaller values to 32-bit representations before proceeding.

Let’s consider the result of 21 & 27.

Binary Decimal

0000 0000 0000 0000 0000 0000 0001 0101

21

&

0000 0000 0000 0000 0000 0000 0001 1011

27

0000 0000 0000 0000 0000 0000 0001 0001

17

Note how this result is different from 21 | 27.

Binary Decimal

0000 0000 0000 0000 0000 0000 0001 0101

21

|

0000 0000 0000 0000 0000 0000 0001 1011

27

0001 1111

31

And also from 21 ^ 27.

Binary Decimal

0000 0000 0000 0000 0000 0000 0001 0101

21

^

0000 0000 0000 0000 0000 0000 0001 1011

27

0000 1110

14

Ignoring overflow, signed left shifting is equivalent to repeated multiplications by 2. Consider 11 << 3. The representation 0000 0000 0000 0000 0000 0000 0000 1011 is shifted to the left to make 0000 0000 0000 0000 0000 0000 0101 1000 = 88 = 11 â‹… 23.

Signed right shifting is equivalent to repeated integer divisions by 2. Consider -104 >> 2. The representation 1111 1111 1111 1111 1111 1111 1001 1000 is shifted to the right to make 1111 1111 1111 1111 1111 1111 1110 0110 = -26 = -104 ÷ 22.

Unsigned right shifting is the same as signed right shifting except when it is done on negative numbers. Since their sign bit is replaced by 0, an unsigned right shift produces a (generally large) positive number. Consider -104 >>> 2. The representation 1111 1111 1111 1111 1111 1111 1001 1000 is shifted to the right to make 0011 1111 1111 1111 1111 1111 1110 0110 = 1,073,741,798.

Because of the way two’s complement is designed, bitwise complement is equivalent to negating the sign of the number and then subtracting 1. Consider ~(-104). The representation 1111 1111 1111 1111 1111 1111 1001 1000 is complemented to 0000 0000 0000 0000 0000 0000 0110 0111 = 103.

Rational numbers

We’ve seen how to represent positive and negative integers in computer memory, but this section shows how rational numbers, such as 12.33, -149.89, and 3.14159, can be converted into binary and represented.

Scientific notation

Scientific notation is closely related to the way a computer represents a rational number in memory. Scientific notation is a tool for representing very large or very small numbers without writing a lot of zeroes. A decimal number in scientific notation is written a × 10b where a is called the mantissa and b is called the exponent.

For example, the number 3.14159 can be written in scientific notation as 0.314159 × 101. In this case, 0.314159 is the mantissa, and 1 is the exponent. Here a few more examples of writing numbers in scientific notation.

3.14159

=

3.14159 × 100

3.14159

=

314159 × 10-5

-141.324

=

-0.141324 × 102

30,000

=

.3 × 105

There are many ways of writing any given number in scientific notation. A more standardized way of writing real numbers is normalized scientific notation. In this notation, the mantissa is always written as a number whose absolute value is less than 10 but greater than or equal to 1. Following are a few examples of decimal numbers in normalized scientific notation.

3.14159

=

3.14159 × 100

-141.324

=

-1.41324 × 103

30,000

=

3.0 × 104

A shorthand for scientific notation is E notation, which is written with the mantissa followed by the letter ‘E’ followed by the exponent. For example, 39.2 in E notation can be written 3.92E1 or 0.392E2. The letter ‘E’ should be read “multiplied by 10 to the power.” It’s legal to use E notation to represent numbers in scientific notation in Java.

Fractions

A rational number can be broken into an integer part and a fractional part. In the number 3.14, 3 is the integer part, and .14 is the fractional part. We’ve already seen how to convert the integer part to binary. Now, we’ll see how to convert the fractional part into binary. We can then combine the binary equivalents of the integer and fractional parts to find the binary equivalent of a decimal real number.

A decimal fraction f is converted to its binary equivalent by successively multiplying it by 2. At the end of each multiplication step, either a 0 or a 1 is obtained as an integer part and is recorded separately. The remaining fraction is again multiplied by 2 and the resulting integer part recorded. This process continues until the fraction reduces to zero or enough binary digits for the desired precision have been found. The binary equivalent of f then consists of the bits in the order they have been recorded, as shown in the next example.

Example 1.13 Fraction conversion to binary

Let’s convert 0.8125 to binary. The table below shows the steps to do so.

Step f 2f Integer part Remainder

1

0.8125

1.625

1

0.625

2

0.625

1.25

1

0.25

3

0.25

0.5

0

0.5

4

0.5

1.0

1

0

We then collect all the integer parts and get 0.11012 as the binary equivalent of 0.8125. We can convert this binary fraction back into decimal to verify that it’s correct.

0.11012 = 1 â‹… 2-1 + 1 â‹… 2-2 + 0 â‹… 2-3 + 1 â‹… 2-4 = 0.5 + 0.25 + 0 + 0.0625 = 0.8125

In some cases the process described above will never have a remainder of 0. Then, we can only find an approximate representation of the given fraction as demonstrated in the next example.

Example 1.14 Non-terminating fraction

Let’s convert 0.3 to binary assuming that we have only five bits in which to represent the fraction. The following table shows the five steps in the conversion process.

Step f 2f Integer part Remainder

1

0.3

0.6

0

0.6

2

0.6

1.2

1

0.2

3

0.2

0.4

0

0.4

4

0.4

0.8

0

0.8

5

0.8

1.6

1

0.6

Collecting the integer parts we get 0.010012 as the binary representation of 0.3. Let’s convert this back to decimal to see how accurate it is.

0.010012 = 0 â‹… 2-1 + 1 â‹… 2-2 + 0 â‹… 2-3 + 0 â‹… 2-4 + 1 â‹… 2-5 = 0 + 0.25 + 0 + 0 + 0.03125 = 0.28125

Five bits are not enough to represent 0.3 fully. Indeed, perfect accuracy would require an infinite number of bits! In this case, we have an error of 0.3 - 0.28125 = 0.01875. Most computers use many more bits to represent fractions and obtain much better accuracy in their representation.

Now that we understand how integers as well as fractions can be converted from one number base to another, we can convert any rational number from one base to another. The next example demonstrates one such conversion.

Example 1.15 Rational number converted to binary

Let’s convert 14.3 to binary assuming that we’ll only use six bits to represent the fractional part. First we convert 14 to binary using the technique described earlier. This gives us 14 = 11102. Taking the method outlined in Example 1.14 one step further, our six bit representation of 0.3 is 0.0100112. Combining the two representations gives 14.3 = 1110.0100112.

Floating-point representation

Floating-point representation is a system used to represent rational numbers in computer memory. In this notation a number is represented as a × be, where a gives the significant digits (mantissa) of the number and e is the exponent. The system is very similar to scientific notation, but computers usually use base b = 2 instead of 10.

For example, we could write the binary number 1010.12 in floating-point representation as 10.1012 × 22 or as 101.012 × 21. In any case, this number is equivalent to 10.5 in decimal.

In standardized floating-point representation, a is written so that only the most significant non-zero digit is to the left of the decimal point. Most computers use the IEEE 754 floating-point representation to represent rational numbers. In this notation, the memory to store the number is divided into three segments: one bit used to mark the sign of the number, m bits to represent the mantissa (also known as the significand), and e bits to represent the exponent.

In IEEE floating-point representation, numbers are commonly represented using 32 bits (known as single precision) or using 64 bits (known as double precision). In single precision, m = 23 and e = 8. In double precision, m = 52 and e = 11. To represent positive and negative exponents, the exponent has a bias added to it so that the result is never negative. This bias is 127 for single precision and 1,023 for double precision. The packing of the sign bit, the exponent, and the mantissa is shown in Figure 1.4 (a) and (b).

Example 1.16 Single precision IEEE format

The following is a step-by-step demonstration of how to construct the single precision binary representation in IEEE format of the number 10.5.

  1. Convert 10.5 to its binary equivalent using methods described earlier, yielding 10.510 = 1010.12. Unlike the case of integers, the sign of the number is taken care of separately for floating-point. Thus, we would use 1010.12 for -10.5 as well.

  2. Write this binary number in standardized floating-point representation, yielding 1.01012 × 23.

  3. Remove the leading bit (always a 1 for non-zero numbers), leaving 0101.

  4. Pad the fraction with zeroes on the right to fill the 23-bit mantissa, yielding 0101 0000 0000 0000 0000 000. Note that the decimal point is ignored in this step.

  5. Add 127 to the exponent. This gives us an exponent of 3 + 127 = 130.

  6. Convert the exponent to its 8-bit unsigned binary equivalent. Doing so gives us 13010 = 100000102.

  7. Set the sign bit to 0 if the number is positive and to 1 otherwise. Since 10.5 is positive, we set the sign bit to 0.

We now have the three components of 10.5 in binary. The memory representation of 10.5 is shown in Figure 1.4 (c). Note in the figure how the sign bit, the exponent, and the mantissa are packed into 32 bits.

numberRepresentationFigure
Figure 1.4 Layouts for floating-point representation (a) in single precision, (b) in double precision, and (c) of 10.510 in single precision.
Largest and smallest numbers

Fixing the number of bits used for representing a real number limits the numbers that can be represented in computer memory using the floating-point notation. The largest rational number that can be represented in single precision has an exponent of 127 (254 after bias) with a mantissa consisting of all 1s:
0 1111 1110 1111 1111 1111 1111 1111 111
This number is approximately 3.402 × 1038. To represent the smallest (closest to zero) non-zero number, we need to examine one more complication in the IEEE format. An exponent of 0 implies that the number is unnormalized. In this case, we no longer assume that there is a 1 bit to the left of the mantissa. Thus, the smallest non-zero single precision number has its exponent set to 0 and its mantissa set to all zeros with a 1 in its 23rd bit:
0 0000 0000 0000 0000 0000 0000 0000 001
Unnormalized single precision values are considered to have an exponent of -126. Thus, the value of this number is 2-23 × 2-126 = 2-149 ≈ 1.4 × 10-45. Now that we know the rules for storing both integers and floating-point numbers, we can list the largest and smallest values possible in 32- and 64-bit representations in Java in the following table. Note that largest means the largest positive number for both integers and floating-point values, but smallest means the most negative number for integers and the smallest positive non-zero value for floating-point values.

Format Largest number Smallest number

32-bit integer

2,147,483,647

-2,147,483,648

64-bit integer

9,223,372,036,854,775,807

-9,223,372,036,854,775,808

32-bit floating-point

3.4028235 × 1038

1.4 × 10-45

64-bit floating-point

1.7976931348623157 × 10308

4.9-324

Using the same number of bits, floating-point representation can store much larger numbers than integer representation. However, floating-point numbers are not always exact, resulting in approximate results when performing arithmetic. Always use integer formats when fractional parts aren’t needed.

Special numbers

Several binary representations in the floating-point representation correspond to special numbers. These numbers are reserved and do not have the values that would be expected from normal multiplication of the mantissa by the power of 2 given by the exponent.

0.0 and -0.0

When the exponent and the mantissa are both 0, the number is interpreted as a 0.0 or -0.0 depending on the sign bit. For example, in a Java program, dividing 0.0 by -1.0 results in -0.0. Similarly, -0.0 divided by -1.0 is 0.0. Positive and negative zeroes only exist for floating-point values. -0 is the same as 0 for integers. Dividing the integer 0 by -1 in Java results in 0 and not in -0.

Positive and negative infinity

An overflow or an underflow might occur while performing arithmetic on floating-point values. In the case of an overflow, the resulting number is a special value that Java recognizes as infinity. In the case of an underflow, it is a special negative infinity value. For example, dividing 1.0 by 0.0 in Java results in infinity and dividing -1.0 by 0.0 results in negative infinity. These values have well defined behavior. For example, adding 1.0 to infinity yields infinity.
Note that floating-point values and integers do not behave in the same way. Dividing the integer 1 by the integer 0 creates an error that can crash a Java program.

Not-a-number (NaN)

Some mathematical operations may result in an undefined number. For example, the square root of a negative number is an imaginary number. Java has a value set aside for results that are not rational numbers. When we discuss how to find the square root of a value in Java, this not-a-number value will be the answer for the square root of a negative number.

Errors in floating-point arithmetic

As we have seen, many rational numbers can only be approximately represented in computer memory. Thus, arithmetic done on the approximate values yields approximate answers. For example, 1.3 cannot be represented exactly using a 64-bit value. In this case, the product 1.3 * 3.0 will be 3.9000000000000004 instead of 3.9. This error will propagate as additional operations are performed on previous results. The next example illustrates this propagation of errors when a sequence of floating-point operations are performed.

Example 1.17 Error propagation

Suppose that the price of several products is added to determine the total price of a purchase at a cash register that uses floating-point arithmetic with a 32-bit variable (the equivalent of a float in Java). For simplicity, let’s assume that all items have a price of $1.99. We don’t know how many items will be purchased ahead of time and simply add the price of each item until all items have been scanned at the register. The table below shows the value of the total cost for different quantities of items.

Items Correct Cost Calculated Cost Absolute Error Relative Error

100

199.0

1.9900015E02

1.5258789E-04

7.6677333E-07

500

995.0

9.9499670E02

3.2958984E-03

3.3124606E-06

1000

1990.0

1.9899918E03

8.1787109E-03

4.1099051E-06

10000

19900.0

1.9901842E04

1.8417969E00

9.2552604E-05

The first column in the table above is the number of items. The second column is the correct cost of all items purchased. The third column is the cost calculated by adding each item using single precision floating-point addition. The fourth and fifth columns give the absolute and relative errors, respectively, of the calculated value. Note how the error increases as the number of additions goes up. In the last row, the absolute error is almost two dollars.

While the above example may seem unrealistic, it does expose the inherent dangers of floating-point calculations. Although the error is less when using double precision representations, it still exists.

1.4. Solution: Buying a computer

We pose a motivating problem in the Problem section near the beginning of most chapters. Whenever there is a Problem section, there is a Solution section near the end in which we give a solution to the earlier problem.

After all the discussion of the hardware, software, and data representation inside of a computer, you might feel more confused about which computer to buy than before. As a programmer, it’s important to understand how data is represented, but this information plays virtually no role in deciding which computer to buy. Unlike most problems in this book, there’s no concrete answer we can give here. Because the development of technology progresses so rapidly, any advice about computer hardware or software has a short shelf-life.

Software is a huge consideration, beginning with the OS. Because the choice of OS usually affects choice of hardware, we’ll start there. The three major choices for a desktop or laptop OS are Microsoft Windows, Apple macOS, and Linux.

Windows is heavily marketed for business use. Windows suffered from many stability and security issues, but Microsoft has worked hard to address these. Apple macOS and the computers it’s installed on are marketed to an artistic and counter-culture population. Linux is popular among tech savvy users. Putting marketing biases aside, the three operating systems have become more similar over time, and most people could be productive using any of the three. The following table lists some pros and cons for each OS.

OS Pros Cons

Microsoft Windows

  • Compatible with the largest number of programs

  • Can be purchased separately from hardware

  • Can run on Apple hardware

  • Expensive

  • Security concerns

Apple macOS

  • Polished user interface

  • Bundled with many useful programs

  • Tested for use on the hardware it comes with

  • Most expensive

  • Many business applications and games are released late or not at all for macOS

  • Difficult to run on non-Apple hardware

Linux

  • Free

  • Runs on almost any hardware

  • Highly customizable

  • Serviced by a community that develops many free applications for it

  • Can be difficult to install or configure

  • Few commercial applications are available for it

  • Limited customer support

Once you’ve decided on an OS, you can pick hardware and other software that’s compatible with it. For macOS, almost all your hardware choices will be computers sold by Apple. For Windows and Linux, you can either have a computer built for you or build your own. Although computer hardware changes quickly, let’s examine some general guidelines.

CPU

Remember that the speed of a CPU is measured in GHz (billions of clock cycles per second). Higher GHz is generally better, but it’s hard to compare performance across different designs of CPU. There’s also a diminishing returns effect: The very fastest, very newest CPUs are often considerably more expensive even if they only provide slightly better performance. It’s usually more cost effective to select a CPU in the middle of the performance spectrum.

Cache size also has a huge effect on performance. The larger the cache, the less often the CPU has to read data from slower memory. Since most new CPUs available today are 64-bit, the question of word size is no longer significant.

Although some specialists may prefer one or the other, both Intel and AMD make powerful, competitive consumer CPUs.

Memory

Memory includes RAM, hard drives, optical drives, and any other storage. RAM is usually easy to upgrade for desktop machines and less easy (though often possible) for laptops. The price of RAM per gigabyte goes down over time. It may be reasonable to start with a modest amount of RAM and then upgrade after a year or two when it becomes cheaper to do so. It takes a little bit of research to get exactly the right kind of RAM for your CPU and motherboard. The amount of RAM is dependent on what you want to do with your system. The minimum amount of RAM to run Microsoft Windows 10 is 1 GB for 32-bit versions and 2 GB for 64-bit versions. The minimum amount of RAM to run Apple macOS Mojave is 2 GB. One rule of thumb is to have at least twice the minimum required RAM.

Hard drive space is dependent on how you expect to use your computer. 1 TB and 2 TB drives are not very expensive, and either represents a huge amount of storage. Only if you plan to have enormous libraries of video or uncompressed audio data will you likely need more. Corporate level databases and web servers and some other business systems can also require vast amounts of space. Hard drive speed is greatly affected by the hard drive’s cache size. As always, a bigger cache means better performance. Using a solid state drive (SSD) instead of a traditional hard drive has much better performance but higher cost per megabyte. If you can afford an SSD, this single upgrade is likely to feel like the greatest increase in overall computer speed.

Installing optical drives and other storage devices depends on individual needs. With the rise of streaming services and cloud backup, optical drives have become less popular.

I/O Devices

The subject of I/O devices is personal. It’s difficult to say what anyone should buy without considering his or her specific needs. A monitor is the essential visual output device while a keyboard and mouse are the essential input devices. Speakers are important as well. Most laptops have all of these elements integrated in some form or another. Laptops often have inexpensive web cameras installed as well.

Someone interested in video games might want to invest in a powerful graphics card. Newer cards with more video RAM are generally better than older cards with less, but which card is best at a given price point is the subject of continual discussion at sites like AnandTech and Tom’s Hardware.

Printers are still useful output devices. Graphics tablets can make it easier to create digital art on a computer. The number of potentially worthwhile I/O devices is limitless.

This section is a jumping off point for purchasing a computer. As you learn more about computer hardware and software, it will become easier to know what combination of the two will serve your needs. Of course, there’s always more to know, and technology changes quickly.

1.4.1. Concurrency: Multicore processors

In the last decade, the word “core” has been splattered all over CPU packaging. Intel in particular has marketed the idea heavily with its older Core and Core 2 models and its modern Core i3, Core i5, Core i7, and Core i9 chips. What are all these cores?

Looking back into the past, most consumer processors had a single core, or brain. They could only execute one instruction at a time. Even this definition is hazy, because pipelining kept more than one instruction in the process of being executed, but overall execution proceeded sequentially.

The advent of multicore processors has changed this design significantly. Each processor has several independent cores, each of which can execute different instructions at the same time. Before the arrival of multicore processors, a few desktop computers and many supercomputers had multiple separate processors that could achieve a similar effect. However, since multicore processors have more than one effective processor on the same silicon die, the communication time between processors is much faster and the overall cost of a multi-processor system is cheaper.

The Good

Multicore systems have impressive performance. The first multicore processors had two cores, but current designs have four, six, eight, or higher. A processor with eight cores can execute eight different programs at the same time. Or, when faced with a computationally intense problem like matrix math, code breaking, or scientific simulation, a processor with eight cores could solve the problem eight times as fast. A desktop processor with 100 cores that can solve a problem 100 times faster is not out of reach.

In fact, modern graphics cards are already blazing this trail. Consider the 1080p standard for high definition video, which has a resolution of 1,920 × 1,080 pixels. Each pixel (short for picture element) is a dot on the screen. A screen whose resolution is 1080p has 2,073,600 dots. To maintain the illusion of smooth movement, these dots should be updated around 30 times per second. Computing the color for more than 2 million dots based on 3D geometry, lighting, and physics effects 30 times a second is no easy feat. Some of the cards used to render computer games have hundreds or thousands of cores. These cores are not general purpose or completely independent. Instead, they’re specialized to do certain kinds of matrix transformations and floating-point computations.

The Bad

Although chip-makers have spent a lot of money marketing multicore technology, they haven’t spent much money explaining that one of the driving forces behind the “multicore revolution” is a simple failure to make processors faster in other ways. In 1965, Gordon Moore, one of the founders of Intel, remarked that the density of silicon microprocessors had been doubling every year (though he later revised this to every two years), meaning that twice as many transistors (computational building blocks) could fit in the same physical space. This trend, often called Moore’s Law, has held up reasonably well. For years, clever designs relying on shorter communication times, pipelining, and other schemes succeeded in doubling the effective performance of processors every two years.

At some point, the tricks became less effective and exponential gains in processor clock rate could no longer be sustained. As clock frequency increases, the signal becomes more chaotic, and it becomes more difficult to tell the difference between the voltages that represent 0s and 1s. Another problem is heat. The energy that a processor uses is related to the square of the clock rate. This relationship means that increasing the clock rate of a processor by a factor of 4 will increase its energy consumption (and heat generation) by a factor of 16.

The legacy of Moore’s Law lives on. We’re still able to fit more and more transistors into tinier and tinier spaces. After decades of increasing clock rate, chip-makers began using the additional silicon density to make processors with more than one core instead. Since 2005 or so, increases in clock rate have stagnated.

The Ugly

Does a processor with eight cores solve problems eight times as fast as its single core equivalent? Unfortunately, the answer is, “Almost never.” Most problems are not easy to break into eight independent pieces.

For example, if you want to build eight houses and you have eight construction teams, then you probably can get pretty close to completing all eight houses in the time it would have taken for one team to build a single house. But what if you have eight teams and only one house to build? You might be able to finish the house a little early, but some steps necessarily come after others: The concrete foundation must be poured and solid before framing can begin. Framing must be finished before the roof can be put on. And so on.

Like building a house, most problems you can solve on a computer are difficult to break into concurrent tasks. A few problems are like painting a house and can be completed much faster with lots of concurrent workers. Other tasks simply cannot be done faster with more than one team on the job. Worse, some jobs can actually interfere with each other. If a team is trying to frame the walls while another team is trying to put the roof onto unfinished walls, neither will succeed, the house might be ruined, and people could get hurt.

On a desktop computer, individual cores generally have their own level 1 cache but share level 2 cache and RAM. If the programmer isn’t careful, he or she can give instructions to the cores that will make them fight with each other, overwriting memory that other cores are using and crashing the program or giving an incorrect answer. Imagine if different parts of your brain were completely independent and fought with one another. The words that came out of your mouth might be gibberish.

To recap, the first problem with concurrent programming is finding ways to break down problems so that they can be solved faster with multiple cores. The second problem is making sure that the different cores cooperate so that the answer is correct and makes sense. These are not easy problems, and many researchers are still working on finding better ways to do both.

Some educators believe that beginners will be confused by concurrency and should wait until later courses to confront these problems. We disagree: Forewarned is forearmed. Concurrency is an integral part of modern computation, and the earlier you get introduced to it, the more familiar it’ll be.

1.5. Summary

This introductory chapter focused on the fundamentals of a computer. We began with a description of computer hardware, including the CPU, memory, and I/O devices. We also described the software of a computer, highlighting key programs such as the operating system and compilers as well as other useful programs like business applications, video games, and web browsers.

Then, we introduced the topic of how numbers are represented inside the computer. Various number systems and conversion from one system to another were explained. We discussed how floating-point representation is used to represent rational numbers. A sound knowledge of data representation helps a programmer decide what kind of data to use (integer or floating-point and how much precision) as well as what kind of errors to expect (overflow, underflow, and floating-point precision errors).

The next chapter extends the idea of data representation into the specific types of data that Java uses and introduces representation systems for individual characters and text.

1.6. Exercises

Conceptual Problems

  1. Name a few programming languages other than Java.

  2. What’s the difference between machine code and bytecode?

  3. What are some advantages of JIT compilation over traditional, ahead-of-time compilation?

  4. Without converting to decimal, how can one find out whether a given binary number is odd or even?

  5. Convert the following positive binary numbers into decimal.

    1. 1002

    2. 1112

    3. 1000002

    4. 1111012

    5. 101012

  6. Convert the following positive decimal numbers into binary.

    1. 1

    2. 15

    3. 100

    4. 1,025

    5. 567,899

  7. What’s the process for converting the representation of a binary integer given in one’s complement into two’s complement?

  8. Perform the conversion from one’s complement to two’s complement on the representation 1011 0111, which uses 8 bits for storage.

  9. Convert the following decimal numbers to their hexadecimal and octal equivalents.

    1. 29

    2. 100

    3. 255

    4. 382

    5. 4,096

  10. Create a table that lists the binary equivalents of octal digits, similar to the one in Section 1.3.2.4. Hint: Each octal digit can be represented as a sequence of three binary digits.

  11. Use the table from Exercise 1.10 to convert the following octal numbers to binary.

    1. 3378

    2. 248

    3. 7778

  12. The ternary number system has a base of 3 and uses symbols 0, 1, and 2 to construct numbers. Convert the following decimal numbers to their ternary equivalents.

    1. 23

    2. 333

    3. 729

  13. Convert the following decimal numbers to 8-bit, two’s complement binary representations.

    1. -15

    2. -101

    3. -120

  14. Given the following 8-bit binary representations in two’s complement, find their decimal equivalents.

    1. 1100 0000

    2. 1111 1111

    3. 1000 0001

  15. Perform the following arithmetic operation on the following 8-bit, two’s complement binary representations of integers. Check your answers by performing arithmetic on equivalent decimal numbers.

    1. 0000 0011 + 0111 1110 =

    2. 1000 1110 + 0000 1111 =

    3. 1111 1111 + 1000 0000 =

    4. 0000 1111 - 0001 1110 =

    5. 1000 0001 - 1111 1100 =

  16. Extrapolate the rules for decimal and binary addition to rules for the hexadecimal system. Then, use these rules to perform the following additions in hexadecimal. Check your answers by converting the values and their sums to decimal.

    1. A2F16 + BB16 =

    2. 32C16 + D11F16 =

  17. Expand Example 1.14 assuming that you have ten bits to represent the fraction. Convert the representation back to base 10. How far off is this value from 0.3?

  18. Will the process in Example 1.14 ever terminate, assuming that we can use as many bits as needed to represent 0.3 in binary? Why or why not?

  19. Derive the binary representation of the following decimal numbers assuming 32-bit (single) precision representation using the IEEE 754 floating-point format.

    1. 0.0125

    2. 7.7

    3. -10.3

  20. The IEEE 754 standard also defines a 16-bit (half) precision format. In this format there is one sign bit, five bits for the exponent, and ten bits for the mantissa. This format is the same as single and double precision in that it assumes that a bit with a value of 1 precedes the ten bits in the mantissa. It also uses a bias of 15 for the exponent. What’s the largest decimal number that can be stored in this format?

  21. Let a, b, and c denote three real numbers. With real numbers, each of the equations below is true. Now suppose that all arithmetic operations are performed using floating-point representations of these numbers. Indicate which of the following equations are still always true and which are sometimes false.

    1. (a + b) + c = a + (b + c)

    2. a + b = b + a

    3. a â‹… b = b â‹… a

    4. a + 0 = a

    5. (a â‹… b) â‹… c = a â‹… (b â‹… c)

    6. a â‹… (b + c) = (a â‹… b) + (a â‹… c)

  22. What’s a multicore microprocessor? Why do you think a multicore chip might be better than a single core chip? Search the Internet to find the specifications for a few common multicore chips. Which chip does your computer use?

2. Problem Solving and Programming

If you can’t solve a problem, then there is an easier problem you can solve: find it.

— George Pólya

2.1. Problem: How to solve problems

How do we solve problems in general? This question is the motivating problem (or meta-problem, even) for this chapter. In fact, this question is the motivating problem for this book. We want to understand the process of solving problems with computers.

As we mentioned in the previous chapter, many computer programs such as business applications and web browsers have already been created to help people solve problems, but we want to solve new problems by writing our own programs. The art of writing these programs is called computer programming or just programming.

Many people reading this book will be computer science students, and that’s great. However, computers have found their way into every segment of commercial enterprise and personal life. Consequently, programming has become a general-purpose skill that can aid almost anyone in their career, whether it’s in transportation, medicine, the military, commerce, or innumerable other areas.

2.1.1. What is a program?

If you don’t have a lot of experience with computer science, writing a computer program may seem daunting. The programs that run on your computer are complex and varied. Where would you start if you wanted to create something like Microsoft Word or Adobe Photoshop?

A computer program is a sequence of instructions that a computer follows. Even the most complex program is just a list of instructions. The list can get very long, and the computer can jump around the list when it makes decisions about what to do next.

Researchers continue to make progress in the field of artificial intelligence, but there is still an enormous gulf between artificial intelligence and human intelligence. Furthermore, it is the software whose intelligence those researchers strive to improve. Computer hardware is neither smart nor stupid because a computer has no intelligence to measure. A computer is a machine like a sewing machine or an internal combustion engine. It follows the instructions we give it blindly. It doesn’t have feelings or opinions about the instructions it receives. Computers do exactly what we tell them to and rarely make mistakes.

Once in a while, a computer will make a mistake due to faulty construction, a bad power source, or cosmic rays, but well over 99.999% of the things that go wrong with computers are because some human somewhere gave a bad instruction. This point highlights one of the most challenging aspects of programming a computer. How do you organize your thoughts so that you express to the computer exactly what you want it to do? If you give a person directions to a drug store, you might say, “Walk east for two blocks and then go into the third door on the right.” The human will fill in all the necessary details: Stopping for traffic, crossing at crosswalks, watching out for construction, and so on. Given a robot body, a computer would do exactly what you say. The instructions never mentioned opening the door, and so a computer might walk right through the closed door, shattering glass in the process.

2.1.2. What is a programming language?

What’s the right level of detail for instructions for a computer? It depends on the computer and the application. But how do we give these instructions? Programs are composed of instructions written in a programming language. In this book, we will use the Java programming language.

Why can’t the instructions be given in English or some other natural language? If the previous example of a robot walking through a glass door didn’t convince you, consider the quote from Groucho Marx, “One morning I shot an elephant in my pajamas. How he got into my pajamas, I’ll never know.”

Natural languages are filled with idioms, metaphors, puns, and other ambiguities. Good writers use these to give life to their poetry and prose, but our goal in this book is not to write poetry or prose. We want to write crystal clear instructions for computers.

Learning Java is not like learning Spanish or Swahili. Like most programming languages, Java is highly structured. It has fewer than 100 reserved words and special symbols. There are no exceptions to its grammatical rules. Don’t confuse the process of designing a solution to a problem with the process of implementing that solution in a programming language. Learning to organize your thoughts into a sequential list of instructions is different from learning how to translate that list into Java or another programming language, but there’s a tight connection between the two.

Learning to program is patterning your mind to think like a machine, to break everything down into simple logical steps. At first, Java code will look like gobbledygook. Eventually, it’ll become so familiar that a glance will tell you volumes about how a program works. Learning to program is not easy, but the kind of logical analysis involved is valuable even if you never program afterward. If you do need to learn other programming languages in the future, it will be easy once you’ve mastered Java.

2.1.3. An example problem

This chapter takes a broad approach to solving problems with a computer, but we need an example to make some of the steps concrete. We use the following problem from physics as an example throughout this chapter.

A rubber ball is dropped on a flat, hard surface from height h. What’s the maximum height the ball will reach after the kth bounce? We’ll discuss the steps needed to create a program to solve this problem in the next section.

2.2. Concepts: Developing software

2.2.1. Software development lifecycle

The engineers who built the first digital computers also wrote the programs for them. In those days, programming was closely tied to the hardware, and the programs were not very long. As the capabilities of computers have increased and programming languages have evolved, computer programs have grown more and more complicated. Hundreds of developers, testers, and managers are needed to write a set of programs as complex as Microsoft Windows 10.

Organizing the creation of such complicated programs is challenging, but the industry uses a process called the software development lifecycle to help. This process makes large projects possible, but we can apply it to the simpler programs we’ll write in this book as well. There are many variations on the process, but we’ll use a straightforward version with the following five steps:

  1. Understand the problem: It seems obvious, but when you go to write a program, all kinds of details need to be worked out. Consider a program that stores medical records. Should the program give an error if a patient’s age is over 150 years? What if advances in long life or cryogenic storage make such a thing possible? What about negative ages? An unborn child (or more outlandishly, someone who had traveled back into the past using a time machine) could be considered to have a negative age.

    Even small details like these must be carefully considered to understand a problem fully. In industry, understanding the problem is often tied to a requirements document, in which a client lays out the features and functionality that the final program should have. The “client” could also be a manager or executive officer of the company you work for who wants your development team to create a certain kind of program. Sometimes the client does not have a strong technical background and creates requirements that are difficult to fulfill or vaguely specified. The creation of the requirements document can be a negotiation process in which the software development team and their client decide together what features are desirable and reasonable.

    If you’re taking a programming class, you can think of your instructor as your client. Then, you can view a programming assignment as a requirements document and your grade as your payment awarded based on how well the requirements were fulfilled.

  2. Design a solution: Once you have a good grasp on the problem, you can begin to design a solution. For large scale projects, this may include decisions about the kinds of hardware and software packages that will be needed to solve the problem. For this book, we’ll only talk about problems that can be solved on standard desktop or laptop computers with Java installed.

    We’ll be interested only in the steps that the computer will have to take to solve the problem. A finite list of steps taken to solve a problem is called an algorithm. If you’ve ever done long division (or any other kind of arithmetic) by hand, you’ve executed an algorithm. The steps for long division will work for any real numbers, and most human beings can follow the algorithm without difficulty.

    An algorithm is often expressed in pseudocode, a high level, generic, code-like system of notation. Pseudocode is similar to a programming language, but it doesn’t have the same detail. Algorithms are often designed in pseudocode so that they aren’t tied to any one programming language.

    When attacking a problem, it’s typical to break it into several smaller subproblems and then create algorithms for the subproblems. This approach is called top-down programming.

  3. Implement the solution: Once you have your solution planned, you can implement it in a programming language. This step entails taking all the pseudocode, diagrams, and any other plans for your solution and translating them into code in a programming language. We use Java for our language in this book, but real developers use whichever language they feel is appropriate.

    If you’ve designed your solution with several parts, you can implement each one separately and then integrate the solutions together. Professional developers often assign different parts of the solution to different programmers or even different teams of programmers.

    Students are often tempted to jump into the implementation step, but never forget that this is the third step of the process. If you don’t fully understand the problem and have a plan to attack it, the implementation process can become bogged down and riddled with mistakes. At first, the problems we introduce and the programs needed to solve them will be simple. As you move into more complicated problems in this book and in your career as a programmer, a good understanding of the problem and a good plan for a solution will become more and more important.

  4. Test the solution: Expressing your algorithm in a programming language is difficult. If your algorithm was wrong, your program won’t always give the right answer. If your algorithm was right but you made a mistake implementing it in code, your program will still be wrong. Programming is a very detail-oriented activity. Even experienced developers make mistakes all the time.

    Good design practices help, but all code must be thoroughly tested after it’s been implemented. It should be tested exhaustively with expected and unexpected input. Tiny mistakes in software called bugs can lie hidden for months or even years before they’re discovered. Sometimes a software bug is a source of annoyance to the user, but other times, as in aviation, automotive, or medical software, people die because of bugs.

    Most of this book is dedicated to designing solutions to problems and implementing them in Java, but Chapter 16 is all about testing and debugging.

  5. Maintenance: Imagine you’ve gone through the previous four steps: You understood all the details of a problem, planned a solution to it, implemented that solution in a programming language, and tested it until it was perfect. What happens next?

    Presumably your program is shipped to your customers and they happily use it. But what if a bug is discovered that slipped past your testing? What if new hardware comes out that’s not compatible with your program? What if your customers demand that you change one little feature?

    Particularly with complex programs that have a large number of consumers, a software development company must spend time on customer support. Responsible software developers are expected to fix bugs, close security vulnerabilities, and polish rough features. This process is called maintenance. Developers are often working on the next version of the product, which could be considered maintenance or a new project entirely.

    Although we cannot stress the importance of the first four steps of the software development lifecycle enough, maintenance is not something we talk about in depth.

The software development lifecycle we presented above is a good guide, but it does not go into details. Different projects require different amounts of time and energy for each step. It’s also useful to focus on the steps because it’s less expensive to fix a problem at an earlier stage in development. It’s impossible to set the exact numbers, but some developers assume that it takes ten times as much effort to fix a problem at the current step than it would have at the previous step.

Example 2.1 Rising costs of fixing an error

Imagine that your company works on computer-aided design (CAD) software. The requirements document for a new program lists the formula for the area of a triangle as base ⋅ height when the real formula is ½ base ⋅ height. If that mistake were caught while understanding the problem, it would mean changing one line of text. Once the solution to the problem has been designed, there might be more references to the incorrect formula. Once the solution has been implemented, those references will have turned into code that is scattered throughout the program. If the project were poorly designed, several different pieces of code might independently calculate the area of a triangle incorrectly. Once the implementation has been tested, a change to the code will mean that everything has to be tested from the very beginning, since fixing one bug can cause other bugs to surface. Finally, once the maintenance stage has been reached, the requirements, plan, implementation, and testing would all need to be updated to fix the bug. Moreover, customers would already have the faulty program. Your company would have to create a patch to fix the bug and distribute it over the Internet.

Most bugs are more complicated and harder to fix, but even this simple one causes greater and greater repercussions as it goes uncaught. A factor of ten for each level implies that it takes 10,000 times more effort to fix it in the maintenance phase than at the first phase. Since fixing it at the first phase would have required a few keystrokes and fixing it in the last phase would require additional development and testing with web servers distributing patches and e-mails apologizing for the mistake, a factor of 10,000 could be a reasonable estimate.

Now that we have a sense of the software development lifecycle, let’s look at an example using the sample ball bouncing problem to walk some of its steps.

Example 2.2 Ball bouncing problem and plan

Recall the statement of the problem from the Problem section:

A rubber ball is dropped on a flat, hard surface from height h. What is the maximum height the ball will reach after the kth bounce?

  1. Understand the problem: This problem requires an understanding of some physics principles. When a ball is dropped, the height of its first bounce depends on a factor known as the coefficient of restitution.

    If c is the coefficient of restitution, then the ball will bounce back the first time to a height of hc. Then, we can act as if the ball were being dropped from this new height when calculating the next bounce. Thus, it will bounce to a height of hc2 the second time. By examining this pattern for the third and fourth bounce, it becomes clear that the ball will bounce to a height of hck on the kth time. See Figure 2.1 for a graphic description of this process.

    We’re assuming that k > 0 and that c < 1. Note that c depends on many factors, such as the elasticity of the ball and the properties of the floor on which the ball is dropped. However, if we know that we will be given c, we don’t need to worry about any other details.

    bouncingBall
    Figure 2.1 A ball is dropped from height h. The ball rises to height hc after the first bounce and to hc2 after the second.
  2. Design a solution: This problem is straightforward, but it’s always useful to practice good design. Remember that you’ve got to plan your input and output as well as the computation in a program. As practice for more complicated problems, let’s break this problem down into smaller subproblems.

    Subproblem 1

    Get the values of h, c, and k from the user.

    Subproblem 2

    Compute the height of the ball after the kth bounce.

    Subproblem 3

    Display the calculated height.

    The solution to each of the three subproblems requires input and generates an output. Figure 2.2 shows how these solutions are connected. The first box in this figure represents the solution to subproblem 1. It asks a user to input values of parameters h, c, and k. It sends these values to the next box, which represents a solution to subproblem 2. This second box computes the height of the ball after k bounces and makes it available to the third box, which represents a solution to subproblem 3. This third box displays the calculated height.

    subProblemRelation
    Figure 2.2 Connections between solutions to the three subproblems in the bouncing ball problem.

Before we can continue on to Step 3, we need to learn some Java. Section 2.3 introduces you to the Java you’ll need to solve this problem.

2.2.2. Acquiring a Java compiler

Before we introduce any Java syntax, you should make sure you have a Java compiler set up so that you can follow along and test your solution. Programming is a hands-on skill. It’s impossible to improve your programming abilities without practice. No amount of reading about programming is a substitute for the real thing.

Where can you get a Java compiler? Fortunately, there are free options for almost every platform. Non-Windows computers may already have the Java Runtime Environment (JRE) installed, allowing you to run Java programs; however, many Java development options require you to have the Java Development Kit (JDK). Oracle may change the website, but at the time of writing you can download the JDK from the Oracle downloads site. Download a current version (e.g., Java SE 8 or 11) of the Java Platform, Standard Edition JDK and install it.

After having done so, you should be able to compile programs using the javac command, whose name is short for “Java compiler.” To do so, open a terminal window, also known as a command line interface or the console. To open the terminal in Windows, choose the Windows Powershell option from the Start Menu. To open the terminal in macOS, select Terminal from the Utilities folder. Different distributions of Linux have different ways of accessing the terminal, but Linux users are usually familiar with their terminal.

Provided that you have your path set correctly, you should be able to open the terminal, navigate to a directory containing files that end in .java, and compile them using the javac command. For example, to compile a program called Example.java to bytecode, you would type:

javac Example.java

Compiling the program creates a .class file, in this case, Example.class. To run the program contained in Example.class, you would type:

java Example

Doing so fires up the JVM, which uses the JIT compiler to compile the bytecode inside Example.class into machine code and run it. Note that you type only Example not Example.class when specifying the program to run. Using just these commands from the terminal, you can compile and run Java programs. The command line interface used to be the only way to interact with a computer, and though it seems primitive at first, the command line has amazing power and versatility.

To use the command line interface to compile your own Java program, you must first create a text file with your Java code in it. The world of programming is filled with many text editor applications whose only purpose is to make writing code easier. These editors are not like Microsoft Word: They are not used to format the text into paragraphs or apply bold or italics. Their output is a “plain” text file, containing only unformatted characters, similar to the files created by Notepad. Some text editors have advanced features useful for programmers, such as syntax highlighting (marking special words and symbols in the language with colors or fonts), language-appropriate auto-completion, and powerful find and replace tools. Two of the most popular text editors for command line use are vi-based editors, particularly Vim, and Emacs-based editors, particularly GNU Emacs.

Many computer users, however, are used to a graphical user interface (GUI), where a mouse can be used to interact with windows, buttons, text boxes, and other elements of a modern user interface. There are Java programming environments that provide a GUI from which a user can write Java code, compile it, execute it, and even test and debug it. Because these tools are integrated into a single program, these applications are called integrated development environments (IDEs).

Two of the most popular Java IDEs are Eclipse and IntelliJ IDEA. Eclipse is open-source, free, and available here. Although some versions of IntelliJ IDEA cost money, the Community edition of IntelliJ IDEA is free and available here.

Which text editor or graphical IDE you use is up to you. Programming is a craft, and every artisan has favorite tools. Most of the content of this book is completely independent from the tools you use to write and compile your code. One exception is Chapter 16, in which we briefly discuss the debugging tools in Eclipse and IntelliJ IDEA.

If you choose Eclipse, IntelliJ IDEA, or another complex IDE, you may wish to read online tutorials to get started. These IDEs often require the user to make a project and then create Java files inside. The idea of a project containing related source code files is a useful one and is very common in software engineering, but it is not a part of Java itself.

2.3. Syntax: Java basics

In this section, we start with the simplest Java programs and work up to the solution to the bouncing ball problem. Syntax is the rules for constructing legal programs, just as English grammar is the rules for constructing legal English sentences. Java was first released in 1995, a long time ago in the history of computer science, but it was based on even older languages. Its syntax inherits ideas from C, C++, and other languages.

Some critics have complained about elements of the syntax or semantics of Java. Semantics are rules for what code means. Remember that Java is an arbitrary system, designed by fallible human beings. The rules for building Java programs are generally logical, but they are artificial. Learning a new programming language is a process of accepting a set of rules and coming up with ways to use those rules to achieve your own ends.

There are reasons behind the rules, but we won’t always be able to explain those reasons in this book. As you begin to learn Java, you’ll have to take it on faith that such-and-such a rule is necessary, even though it seems useless or mysterious. In time, these rules will become familiar and perhaps sensible. The mystery will fade away, and the rules will begin to look like an organic and logical (though imperfect) system.

2.3.1. Java program structure

The first rule of Java is that all code goes inside of a class. A class is a container for blocks of code called methods, and it can also be used as a template to create objects. We’ll talk a bit more about classes in this chapter and then focus on them heavily in Chapter 9.

For now, you only need to remember that every Java program has at least one class. It is possible to put more than one class in a file, but you can only have one top-level public class per file. A public class is one that can be used by other classes. Almost every class in this book is public, and they should be clearly marked as such. To create a public class called Example, you would type the following:

public class Example {
}

A few words in Java have a special meaning and cannot be used for other purposes (like the name you give a class). These are called keywords or reserved words. The keyword public marks the class as public. The keyword class states that you are declaring a class. All keywords are lowercase in Java.

The name Example gives the name of the class. By convention, all class names start with a capital letter. The braces ({ and }) mark the start and end of the contents of the class. Right now, our class contains nothing.

This text should be saved in a file called Example.java. It’s required that the name of the public class matches the file that it’s in, including capitalization. Once Example.java has been saved, you can compile it into bytecode. However, since there’s nothing in class Example, you can’t run it as a program.

A program is a list of instructions, and that list has to start somewhere. For a normal Java application, that place is the main() method. Throughout this book, we always append parentheses () to mark the name of a method. If we want to do something inside of Example, we’ll have to add a main() method like so:

public class Example {
    public static void main(String[] args) {
    }
}

There are several new items now. As before, public means that other classes can use the main() method. The static keyword means that the method is associated with the class as a whole and not a particular object. The void keyword means that the method does not give back a result. The word main is obviously the name of the method, but it has to be spelled exactly that way (including capitalization) to work. Perhaps the most confusing part is the expression String[] args, which is a list of text (strings) given as input to the main() method from the command line. As with the class, the braces mark the start and end of the contents of the main() method, which is currently empty.

Right now, you don’t need to understand any of this. All you need to know is that, to start a program, you need a main() method and its syntax is always the same as the code listed above. If you save this code, you can compile Example.java and then run it, and…​nothing happens! It’s a perfectly legal Java program, but the list of instructions is empty.

2.3.2. Command line input and output

An important thing for a Java program to do is to communicate with the outside world (where humans live). First, let’s look at printing data to the command line and reading data in from the command line.

The System.out.print() method

Methods allow us to perform actions in Java. They are blocks of code packaged up with a name so that we can run the same piece of code repeatedly but with different inputs. We discuss them in much greater depth in Chapter 8.

A common method for output is System.out.print(). The input (usually called arguments) to a method are given between its parentheses. Thus, if we want to print 42 to the terminal, we type:

System.out.print(42);

Note that the use of the method has a semicolon (;) after it. An executable line of code in Java generally ends with a semicolon to separate it from the next instruction. We can add this code to our Example class, yielding:

public class Example {
    public static void main(String[] args) {
        System.out.print(42);
    }
}

If we want to print out text, we give it as the argument to System.out.print(), surrounded by double quotes ("). It’s necessary to surround text with quotes so that Java knows it’s text and not the name of a class, method, or variable. Text surrounded by double quotes is called a string. The following program prints Forty two onto the terminal.

public class Example {
    public static void main(String[] args) {
        System.out.print("Forty two");
    }
}

Printing out one thing is great, but programs are usually composed of many instructions. Consider the following program:

public class Example {
    public static void main(String[] args) {
        System.out.print(2);
        System.out.print(4);
        System.out.print(6);
        System.out.print(8);
    }
}

As you can see, each executable line ends with a semicolon, and they are executed in sequence. This code prints 2, 4, 6, and 8 onto the screen. However, we did not tell the program to move the cursor to a new line at any point so the output on the screen is 2468, which looks like a single number. If we want them to be on separate lines, we can achieve this with the System.out.println() method, which moves to a new line after it finishes output.

public class Example {
    public static void main(String[] args) {
        System.out.println(2);
        System.out.println(4);
        System.out.println(6);
        System.out.println(8);
    }
}

This change makes the output into the following:

2
4
6
8

In Java, it’s possible to insert math almost anywhere. Consider the following program, which uses the + operator.

public class Example {
    public static void main(String[] args) {
        System.out.print(35 + 7);
    }
}

This code prints out 42 to the terminal just like our earlier example, because it does the addition before giving the result to System.out.print() for output.

The Scanner class

We want to be able to read input from the user as well. For command line input, we need to create a Scanner object. This object is used to read data from the keyboard. The following program asks the user for an integer, reads in an integer from the keyboard, and then prints out the value multiplied by 2.

import java.util.Scanner;

public class Example {
    public static void main(String[] args) {
        Scanner in;
        in = new Scanner(System.in);
        System.out.print("Enter an integer: ");
        int value;
        value = in.nextInt();
        System.out.print("That number doubled is: ");
        System.out.println(value * 2);
    }
}

This program introduces several new elements. First, note that it begins with
import java.util.Scanner;. This line of code tells the compiler to use the Scanner class that is in the java.util package. A package is a way of organizing a group of related classes. Someone else wrote the Scanner class and all the other classes in the java.util package, but by importing the package, we’re able to use their code in our program.

Then, skip down to the first line in the main() method. The code Scanner in; declares a variable called in with type Scanner. A variable can hold a value. The variable has a specific type, meaning the kind of data that the value can be. In this case the type is Scanner, meaning that the variable in holds a Scanner object. Declaring a variable creates a box that can hold things of the specified type. To declare a variable, first put the type you want it to have, then put its identifier or name, and then put a semicolon. We chose to call the variable in, but we could have called it input or even marmalade if we wanted. It’s always good practice to name your variable so that it’s clear what it contains.

The next line assigns a value to in. The assignment operator (=) looks like an equal sign from math, but think of it as an arrow that points left. Whatever’s on the right side of the assignment operator will be stored into the variable on the left. The variable in was an initially empty box that could hold a Scanner object. The code new Scanner(System.in) creates a brand new Scanner object based on System.in, which means that the input will be from the keyboard. The assignment stored this object into the variable in. The fact that System.in was used has nothing to do with the fact that our variable was named in. Again, don’t expect to understand all the details at first. Any time you need to read data from the keyboard, you’ll need these two lines of code, which you should be able to copy verbatim. It’s possible to both declare a variable and assign its value in one line. Instead of the two line version, most programmers would type:

Scanner in = new Scanner(System.in);

Similarly, the line int value; declares a variable for holding integer types. The next line uses the object in to read an integer from the user by calling the nextInt() method. If we wanted to read a floating-point value, we would have called the nextDouble() method. If we wanted to read some text, we would have called the next() method. Unfortunately, these differences means that we have to know what type of data the user is going to enter. If the user enters an unexpected type, our program could have an error. As before, we could combine the declaration and the assignment into a single line:

int value = in.nextInt();

The final two lines give output for our program. The former prints That number doubled is: to the terminal. The latter prints out a value that is twice whatever the user entered. The next two examples illustrate how Scanner can be used to read input while solving problems. The first example shows how these elements can be applied to subproblem 1 of the bouncing ball problem, and the second example introduces and solves a new problem.

Example 2.3 Command line input

Subproblem 1 requires us to get the height, coefficient of restitution, and number of bounces from the user. Program 2.1 shows a Java program to solve this subproblem.

Program 2.1 A Java program to get the height, coefficient of restitution, and number of bounces from the command line.
import java.util.*; (1)

public class GetInputCLI { (2)
    public static void main(String[] args) { (3)
        // Create an object named in for input
        Scanner in = new Scanner(System.in); (4)
        
        // Declare variables to hold input data
        double height, coefficient; (5)
        int bounces; 
        
        // Prompt the user and read data from the keyboard 
        System.out.println("Bouncing Ball: Subproblem 1"); (6)
        System.out.print("Enter the height: "); 
        height = in.nextDouble(); (7)
        System.out.print("Enter restitution coefficient: "); (8)
        coefficient = in.nextDouble();
        System.out.print("Enter the number of bounces: "); 
        bounces = in.nextInt();
    } 
}
1 Unlike our earlier example, the first line of GetInputCLI.java is import java.util.*;. Instead of importing only the Scanner class, this line imports all the classes in the java.util package. The asterisk (*) is known as a wildcard. The wildcard notation is convenient if you need to import several classes from a package or if you don’t know in advance the names of all the classes you’ll need.
2 The class declaration names the class GetInputCLI. We put a CLI at the end of the name to mark that it uses the command line interface, contrasting with the GUI version that we’re going to show next.
3 Inside the class declaration is the definition of the main() method, showing where the program starts. The text that comes after double slashes (//) is called a comment. Comments allow us to make our code more readable to humans, but the compiler ignores them.
4 After the comments, we declare and instantiate a Scanner variable called in for reading from the keyboard.
5 Next, we declare two double variables (for holding double precision floating-point numbers) and an int variable (for holding an integer value).
6 We print out the name of the problem and then print out "Enter the height: ".
7 The line height = in.nextDouble(); tries to read in the height from the user. It waits until the user hits enter before reading the value and moving on to the next line.
8 The last four lines of the program prompt and read in the coefficient of restitution and then the number of bounces. If you compile and run this program, the execution should match the steps described. Note that it only reads in the values needed to solve the problem. We haven’t added the code to compute the answer or display it.
Example 2.4 Input for distance computation

Let’s write a program that takes as input the speed of a moving object and the time it’s been moving. The goal is to compute and display the total distance it travels. We can divide this problem into the following three subproblems.

Subproblem 1

Input speed and duration.

Subproblem 2

Compute distance traveled.

Subproblem 3

Display the computed distance.

Program 2.2 solves each of these subproblems in order, using the command-line input and output tools we have just discussed.

Program 2.2 Computes the distance a moving object travels.
import java.util.*; (1)

public class Distance {
    public static void main(String[] args) {
        // Create an object named in for input
        Scanner in = new Scanner(System.in);  (2)
        double speed, time;  
        double distance; // Distance to be computed       
        
        // Solution to subproblem 1: Read input
        // Prompt the user and get speed and time
        System.out.print("Enter the speed: "); (3)
        speed = in.nextDouble();
        System.out.print("Enter the time: ");
        time = in.nextDouble();
        
        // Solution to subproblem 2: Compute distance
        distance = speed*time; (4)

        // Solution to subproblem 3: Display output
        System.out.print("Distance traveled: "); (5)
        System.out.print( distance );
        System.out.println(" miles.");
    }
}
1 The program starts with import statements, the class definition, and the definition of the main() method.
2 At the beginning of the main() method, we have code to declare and initialize a variable of type Scanner named in. We also declare variables of type double to hold the input speed and time and the resulting distance.
3 We start solving subproblem 1, prompting the user for the speed and the time and using our Scanner object to read them in. Because they are both floating-point values with type double, we use the nextDouble() method for input.
4 We compute the distance traveled by multiplying speed by time and storing the result in distance.
5 The last three lines of the main() method solve subproblem 3 by outputting "Distance traveled: ", the computed distance, and " miles.". If you run the program, all three items are printed on the same line on the terminal.

2.3.3. GUI input and output

If you’re used to GUI-based programs, you might wonder how to do input and output with a GUI instead of on the command line. GUIs can become complex, but in this chapter we introduce a simple way to do GUI input and output and expand on it further in Chapter 7. Then, we go into GUIs in much more depth in Chapter 15.

A limited tool for displaying output and reading input with a GUI is the JOptionPane class. This class has a complicated method called showMessageDialog() that opens a small dialog box to display a message to the user. If we want to create the equivalent of the command-line program that displays the number 42, the code would be as follows.

import javax.swing.JOptionPane; (1)

public class Example {
    public static void main(String[] args) {
        JOptionPane.showMessageDialog(null, "42", "Output Example", (2)
            JOptionPane.INFORMATION_MESSAGE);
	}
}
1 Like Scanner, we need to import JOptionPane as shown above in order to use it.
2 The showMessageDialog() method takes several arguments to tell it what to do. For our purposes, the first one is always the special Java literal null, which represents an empty value. The next is the message you want to display, but it has to be text. That’s why "42" appears with quotation marks. The third argument is the title that appears at the top of the dialog. The final argument gives information about how the dialog should be presented. JOptionPane.INFORMATION_MESSAGE is a flag values that specifies that the dialog is giving information (instead of a warning or a question), causing an appropriate, system-specific icon to be displayed on the dialog.

Figure 2.3 shows what the resulting GUI might look like.

showMessageDialog
Figure 2.3 Example of showMessageDialog() used for output.

One way to do input with a GUI uses the showInputDialog() method, which is also inside the JOptionPane class. The showInputDialog() method returns a value. This means it gives back an answer which you can store into a variable by putting the method call on the right hand side of an assignment statement. Otherwise, it’s nearly the same as showMessageDialog(). The following program prompts the user for his or her favorite word with a showInputDialog() method and then displays it again using a showMessageDialog() method.

import javax.swing.JOptionPane;

public class Example {
    public static void main(String[] args) {
        String message = "What is your favorite word?";
        String title = "Input Example";
        String word =
        JOptionPane.showInputDialog(null, message, title, JOptionPane.QUESTION_MESSAGE);
        JOptionPane.showMessageDialog(null, word, title, JOptionPane.INFORMATION_MESSAGE);
    }
}

Note that whatever the user typed in will be stored in word. Finally, the last line of the program displays this information with showMessageDialog(). Figure 2.4 shows the GUI as the user is entering input.

showInputDialog
Figure 2.4 Example of showInputDialog() used for input.

Remember that the value returned from the showInputDialog() method is always text; that is, it always has type String. Although there are lots of great things you can do with a String value, you can’t do normal arithmetic like you can with an integer or a floating-point number. However, there are ways to convert a String representation of a number to the number itself. If you have a String that represents an integer, you use the Integer.parseInt() method to convert it. If you have a String that represents a floating-point number, you use the Double.parseDouble() method to convert it. The following segment of code illustrates these issues.

// Text cannot be multiplied by an integer
int x = "41" * 3;

// Correctly converts the text "23" to the integer 23
int y = Integer.parseInt("23");

// Correctly converts the text "3.14159" to 3.14159
double z = Double.parseDouble("3.14159");

// Causes the program to crash
int a = Integer.parseInt("Twenty three");

// Causes the program to crash
double b = Double.parseDouble("pi");

You might wonder why the computer isn’t smart enough to know that "23" means 23. Remember, the computer has no intelligence. If something is marked as text, it doesn’t know that it can interpret it as a number. What kind of data something is depends on its type, which doesn’t change. We’ll discuss types more deeply in Chapter 3.

The next example uses these two type conversion methods with methods from JOptionPane in a GUI-based solution to subproblem 1 of the bouncing ball problem.

Example 2.5 GUI input

We can change the solution given in Program 2.1 to use the GUI-based input tools in JOptionPane. Program 2.3 is the equivalent GUI-based Java program.

Program 2.3 Gets the height, coefficient of restitution, and number of bounces using a GUI.
import javax.swing.*;

public class GetInputGUI {
    public static void main(String[] args) {
        String title = "Bouncing Ball: Subproblem 1";
        
        // Declare variables to hold input data
        double height, coefficient;
        int bounces; 
        
        // Prompt the user, get data, and convert it
        String response = JOptionPane.showInputDialog(null, (1)
            "Enter the height: ", title, JOptionPane.QUESTION_MESSAGE);
        height = Double.parseDouble(response); (2)
        response = JOptionPane.showInputDialog(null, (3)
            "Enter restitution coefficient: ", title, JOptionPane.QUESTION_MESSAGE);
        coefficient = Double.parseDouble(response);
        response = JOptionPane.showInputDialog(null,
            "Enter the number of bounces: ", title,
			JOptionPane.QUESTION_MESSAGE);
        bounces = Integer.parseInt(response);
    } 
}
1 At this point the code uses the showInputDialog() method to read a String version of the height from the user.
2 On the next line, we have to convert this String version into the double version that we store in the height variable.
3 The next four lines read in the coefficient of restitution and the number of bounces and convert them to their appropriate numerical types.

2.3.4. A few operations

Basic math

To make our code useful, we can perform operations on values and variables. For example, we used the expression 35 + 7 as an argument to the System.out.print() method to print 42 to the screen. We can use the add (+), subtract (-), multiply (*), and divide(/) operators on numbers to solve arithmetic problems. These operators work the way you’d expect them to (except that division has a few surprises). We’ll go into these operators and others more deeply in Chapter 3. Here are examples of these four operators used with integer and floating-point numbers.

int a = 2 + 3;         // a will hold 5
int b = 2 - 3;         // b will hold -1
int c = 2 * 3;         // c will hold 6
int d = 2 / 3;         // d will hold 0 (explained later)

double x = 1.6 + 3.2;  // x will hold 4.8
double y = 1.6 - 3.2;  // y will hold -1.6
double z = 1.6 * 3.2;  // z will hold 5.12
double w = 1.6 / 3.2;  // w will hold 0.5
Other operations

These basic operations can mix values and variables together. As we’ll discuss later, they can be arbitrarily complicated with order of operations determining the final answer. Nevertheless, we also need ways to accomplish other mathematical operations such as raising a number to a power or finding its square root. The Math class has methods that perform these and other functions. To raise a number to a power, we call Math.pow() with two arguments: first the base and then the exponent. To find a square root, we pass a number to the Math.sqrt() method.

// Raises 3.0 to the power 2.5, approximately 15.588457
double p = Math.pow(3.0, 2.5);

// Finds the square root of 2.0, approximately 1.4142136
double q = Math.sqrt(2.0);
Example 2.6 Compute height

We compute the final height of the ball in subproblem 2 of the bouncing ball problem. To do so, we have to multiply the height by the coefficient of restitution raised to the power of the number of bounces. The following program does so, using the Math.pow() method.

Program 2.4 Computes height of a ball after bounces.
public class ComputeHeight {
    public static void main(String[] args) {
        // Use dummy values to test subproblem 2
        double height = 15, coefficient = 0.3;
        int bounces = 10;
        // Compute height after bounces
        double bounceHeight = height*Math.pow(coefficient,bounces);
        System.out.println(bounceHeight); // For testing
    }
}

Program 2.4 is only focusing on subproblem 2, but, if we want to test it, we need to supply some dummy values for height, coefficient, and bounces, since these are read in by the solution to subproblem 1. Likewise, the output statement on the last line of the main() method is just for testing purposes. The complete solution has more complex output.

String concatenation

Just as we can add numbers together, we can also “add” pieces of text together. In Java, text has the type String. If you use the + operator between two values or variables of type String, the result is a new String that is the concatenation of the two previous String values, meaning that the result is the two pieces of text pasted together, one after the other. Concatenation doesn’t change the String values you’re concatenating.

The results may be illegal or at least unexpected if you mix types (String, int, double) together when doing mathematical operations. However, feel free to concatenate String values with any other type using the + operator. When you do so, the other type is automatically converted into a String. This behavior is useful since any String is easy to output. Here are a few examples of String concatenation.

String word1 = "tomato";
String word2 = "sauce";
String text1 = word1 + word2; 			// text1 contains "tomatosauce"
String text2 = word1 + " " + word2; 	// text2 contains "tomato sauce"
String text3 = "potato " + word1;		// text3 contains "potato tomato"
String text4 = 5 + " " + word1 + "es"; 	// text4 contains "5 tomatoes"
Example 2.7 Display height

With String concatenation, subproblem 3 becomes a bit easier. We concatenate the results together with an appropriate message and then use the System.out.println() method for output.

Program 2.5 Displays height of a ball using the command line.
public class DisplayHeightCLI {
    public static void main(String[] args) {
        // Use dummy values to test subproblem 3        
        int bounces = 10;
        double bounceHeight = 2.0;
        String message = "After " + bounces +
            " bounces the height of the ball is: " + bounceHeight + " feet";
        System.out.println(message);
    }   
}

Program 2.5 is only focusing on subproblem 3, but if we want to test it, we need to supply dummy values for bounces and bounceHeight, since these are generated by the solution to earlier subproblems.

The same concatenation can be used for GUI output as well. The only difference is the use of
JOptionPane.showMessageDialog() instead of System.out.println().

Program 2.6 Displays height of a ball using a GUI.
import javax.swing.*;

public class DisplayHeightGUI {
    public static void main(String[] args) {
        String title = "Bouncing Ball: Subproblem 3";
        // Use dummy values to test subproblem 3
        int bounces = 10;
        double bounceHeight = 2.0;   
        String message = "After " + bounces +
            " bounces the height of the ball is: " + bounceHeight + " feet";
        JOptionPane.showMessageDialog(null, message, title,
            JOptionPane.INFORMATION_MESSAGE);
    }
}

2.3.5. Java formatting

Writing good Java code has some similarities to writing effectively in English. There are rules you have to follow in order to make sense, but there are also guidelines you should follow to make your code easier to read for yourself and everyone else.

Variable and class naming

Java programs are filled with variables, and each variable should be named to reflect its contents. Variable names are essentially unlimited in length (although the JVM you use may limit this length to thousands of characters). A tremendously long variable name can be hard to read, but abbreviations can be worse. You want the meaning of your code to be obvious to others and to yourself when you come back days or weeks later.

Imagine you’re writing a program that sells fruit. Consider the following names for a variable that keeps track of the number of apples.

Name Attributes

a

Too short, gives no useful information

apps

Too short, vague, could mean applications or appetizers

cntr

Too short, vague, could mean center

counter

Not bad, but counting what?

theVariableUsedToCountApples

Too long for no good reason

appleCounter

Very clear

apples

Concise and clear, unless there are multiple apple quantities such as applesSold and applesBought

Mathematics is filled with one letter variables, partly because there’s a history of writing mathematics on chalkboards and paper. Clarity is more important than brevity with variables in computer programs. Some variables need more than one word to be descriptive. In that case, programmers of Java are encouraged to follow camel case. In camel case, multi-word variables and methods start with a lowercase letter and then use an uppercase letter to mark the beginning of each new word. It’s called camel case because the uppercase letters are reminiscent of the humps of a camel. Examples include lastValue, symbolTable, and makeHamSandwich().

By convention, class names should always begin with a capital letter, but they also use camel case, marking the beginning of each new word with an uppercase letter. Examples include LinkedList, JazzPiano, and GlobalNuclearWarfare.

Another convention is that constants, variables whose values never change, have names in all uppercase, separated by underscores. Examples include PI, TRIANGLE_SIDES, and UNIVERSAL_GRAVITATIONAL_CONSTANT.

Spaces are not allowed in variable, method, or class names. Recall that a name in Java is called an identifier. The rules for identifiers specify that they must start with an uppercase or lowercase letter (or an underscore) and that the remaining characters must be letters, underscores, or numerical digits. Thus, Tupac and the absurd _5 are legal identifiers, but Motley Crue and 2Pac are not.

In Java, letters can mean more than just the Latin letters A through Z. Java has support for many of the world’s languages, allowing identifiers to contain characters from Chinese, Thai, Devanagari, Cyrillic, and other scripts. For example, m\u00F6tleyCr\u00FCe is a legal variable name because \u00F6 is way of encoding ö and \u00FC is a way of encoding ü. In some systems, this variable name might be rendered mötleyCrüe, but the compiler could complain if you type those characters in directly. In short, Java supports a huge range of characters, but making that support work for you is sometimes more challenging. Section 3.3.2.10 discusses character encoding further.

Remember that keywords also cannot be used as identifiers. For example, public, static, and class are all keywords in Java and can never be the names of classes, variables, or methods.

White space

Although you are not allowed to have spaces in a Java identifier, you can usually use white space (spaces, tabs, and new lines) wherever you want. Java ignores extra space. Consider the following line of code.

int x = y + 5;

It’s equivalent to the next one.

int x=y+5;

We chose to type our earlier example of a program performing output as follows.

public class Example {
    public static void main(String[] args) {
        System.out.print(42);
    }
}

However, we could have been more chaotic with our use of whitespace.

        public
class          Example {
public
    static void
    main (String     [
        ] args
        ) {
            System.
    out
        .print(42

) ; } }

Or we could have used almost no whitespace at all.

public class Example{public static void main(String[]args){System.out.print(42);}}

These three programs are identical in the eyes of the Java compiler, but the first one is easier for a human to read. You should use whitespace to increase readability. Don’t add too much whitespace with lots of blank lines between sections of code. On the other hand, don’t use too little and cramp the code together. Whenever code is nested inside of a set of braces, indent the contents so that it’s easy to see the hierarchical relationship.

The style we present in this book puts the left brace ({) on the line starting a block of code. Another popular style puts the left brace on the next line. Here is the same example program formatted in this style:

public class Example
{
    public static void main(String[] args)
    {
        System.out.print(42);
    }
}

There are people (including some authors of this book) who prefer this style because it’s easier to see where blocks of code begin and end. However, the other style uses less space, so we use it throughout the book. You can make your own choices about style, but be consistent! If you work for a software development company, they may have strict standards for code formatting.

Comments

As we mentioned before, you can leave comments in your code whenever you want someone reading the code to have extra information. Java has three different kinds of comments. We described single-line comments, which start with a // and continue until the end of the line.

If you have a large block of text you want as a comment, you can create a block comment, which starts with a /* and continues until it reaches a */.

Beyond leaving messages for other programmers, you can also “comment out” existing code. By putting Java code inside a comment, it no longer affects program execution. This practice is common when programmers want to remove or change some code but are reluctant to delete it until the new version of the code has been tested. Don’t overuse the practice of commenting out code! Large programs become hard to navigate when they’re cluttered with many chunks of commented-out code.

The third kind of comment is called a documentation comment and superficially looks a lot like a block comment. A documentation comment starts with a /** and ends with a */. These comments are supposed to come at the beginning of classes and methods and explain what they’re used for and how to use them. A tool called javadoc is used to run through documentation comments and generate an HTML file that users can read to understand how to use the code. This tool is a feature that has contributed greatly to the popularity of Java, keeping its libraries well-documented and easy to use. However, we do not discuss documentation comments deeply in this book.

Here is our example output program, heavily commented.

/**
 *  Class Example prints the number 42 to the screen.
 *  It contains an executable main() method.
 */
public class Example {
    /*
     * The main() method was last updated by Barry Wittman.
     */
    public static void main(String[] args) {
        System.out.print(42);  // answer to everything
    }
}

Comments are a wonderful tool, but clean code with meaningful variable names and careful use of whitespace doesn’t require too much commenting. Never hesitate to comment, but always ask yourself if there is a way to write the code so clearly that a comment is unnecessary.

2.4. Solution: How to solve problems

The problem solving steps given in Section 2.2 are sound, but they depend on being able to implement your planned solution in Java. In this chapter we have introduced far too little Java to expect to solve all the problems that can be solved with a computer. However, we can show the solution to the bouncing ball problem and explain how our solution works through the software development lifecycle.

2.4.1. Bouncing ball solution (command line version)

In Example 2.2, we made sure we understood the problem and then formed a three-part plan to read in the input, compute the height of the bounce, and then output it.

In Program 2.1, we implemented subproblem 1, reading the input from the command line. In Program 2.4, we implemented subproblem 2, computing the height of the final bounce. In Program 2.5, we implemented subproblem 3, displaying the height that was computed. In the final, integrated program, the portion of the code that corresponds to solving subproblem 1 is below.

import java.util.*; 

public class BouncingBallCLI {
    public static void main(String[] args) {
        // Solution to subproblem 1
        // Create an object named in for input
        Scanner in = new Scanner(System.in);
        
        // Declare variables to hold input data
        double height, coefficient;
        int bounces;
 
        System.out.println("Bouncing Ball");
        
        // Prompt the user and read data from the keyboard
        System.out.println("Bouncing Ball: Subproblem 1");
        System.out.print("Enter the height: "); 
        height = in.nextDouble();
        System.out.print("Enter restitution coefficient: "); 
        coefficient = in.nextDouble();
        System.out.print("Enter the number of bounces: "); 
        bounces = in.nextInt();

With the imports, class declaration, and main() method set up by the solution to subproblem 1, the solution to subproblem 2 is very short.

        // Solution to subproblem 2
        double bounceHeight = height*Math.pow(coefficient,bounces);

The solution to subproblem 3 and the braces that mark the end of the main() method and then the end of the class only take up a few more lines.

        // Solution to subproblem 3
        String message = "After " + bounces +
            " bounces the height of the ball is: " + bounceHeight + " feet";
        System.out.println(message);
    }   
}

2.4.2. Bouncing ball solution (GUI version)

If you prefer a GUI for your input and output, we can integrate the GUI-based versions of the solutions to subproblems 1, 2, and 3 from Program 2.1, Program 2.4, and Program 2.6. The final program is below. It only differs from the command line version in a few details.

Program 2.7 Full program to compute the height of the final bounce of a ball and display the result with a GUI.
import javax.swing.*;

public class BouncingBallGUI {
    public static void main(String [] args) {
        // Solution to sub-problem 1
        String title = "Bouncing Ball";
        double height, coefficient;
        int bounces; 
        
        // Prompt the user, get data, and convert it
        String response = JOptionPane.showInputDialog(null,
            "Enter the height: ", title, JOptionPane.QUESTION_MESSAGE);
        height = Double.parseDouble(response);
        response = JOptionPane.showInputDialog(null,
            "Enter restitution coefficient: ", title, JOptionPane.QUESTION_MESSAGE);
        coefficient = Double.parseDouble(response); 
        response = JOptionPane.showInputDialog(null,
            "Enter the number of bounces: ", title, JOptionPane.QUESTION_MESSAGE);
        bounces = Integer.parseInt(response);

        // Solution to sub-problem 2
        double bounceHeight = height*Math.pow(coefficient,bounces);
        
        // Solution to sub-problem 3     
        String message = "After " + bounces +
            " bounces the height of the ball is: " + bounceHeight + " feet";
        JOptionPane.showMessageDialog(null,
            message, title, JOptionPane.INFORMATION_MESSAGE);       
    }  
}

2.4.3. Testing and maintenance

Testing and maintenance are key elements of the software engineering lifecycle and often take more time and resources than the rest. However, we only discuss them briefly here.

The ball bouncing problem is not complex. There are a few obvious things to test. We should pick a “normal” test case such as a height of 15 units, a coefficient of restitution of 0.3, and 10 bounces. The height should be 15 â‹… 0.310 = 0.0000885735. The result computed by our program should be the same, ignoring floating-point error. We can also check some boundary test cases. If the coefficient of restitution is 1, the ball should bounce back perfectly, reaching whatever height we input. If the coefficient of restitution is 0, the ball doesn’t bounce at all, and the final height should be 0.

Our code does not account for users entering badly formatted data like two instead of 2. Likewise, our code does not detect invalid values such as a coefficient of restitution greater than 1 or a negative number of bounces. An industrial-grade program should. We’ll discuss testing further in Chapter 16.

As with most of the problems we discuss in this book, issues of maintenance will not apply: we don’t have a customer base to keep happy. Even so, it’s a good thought exercise to imagine a large-scale version of this program that can solve many different kinds of physics problems. Who are likely to be your clients? What kinds of bugs are likely to creep into such a program? How would you provide bug-fixes and develop new features?

2.5. Concurrency: Solving problems in parallel

2.5.1. Parallelism and concurrency

The terms parallelism and concurrency are often confused and sometimes used interchangeably. Parallelism or parallel computing occurs when multiple computations are being performed at the same time. Concurrency occurs when multiple computations may interact with each other. The distinction is subtle since many parallel computations are concurrent and vice versa.

An example of parallelism without concurrency is two separate programs running on a multicore system. They are both performing computations at the same time, but for the most part, they aren’t interacting with each other. Concurrency issues might arise if these programs try to access a shared resource, such as a file, at the same time.

An example of concurrency without parallelism is a program with multiple threads of execution running on a single-core system. These threads will not execute at the same time as each other. However, the OS or run-time system will interleave the execution of these threads, switching back and forth between them whenever it wants to. Since these threads can share memory, they can still interact with each other in complicated and often unpredictable ways.

With multicore computers, we want good and effective parallelism, computing many things at the same time. Unfortunately, striving to reach parallelism often means struggling with concurrency and carefully managing the interactions between threads.

2.5.2. Sequential versus concurrent programming

Imagine that the evil Lellarap aliens are attacking Earth. They have sent an extensive list of demands to the world’s leaders, but only a few people, including you, have mastered their language, Lellarapian. To save the people of Earth, it’s imperative that you translate their demands as quickly as possible so world leaders can choose a course of action. If you do it alone, as illustrated in Figure 2.5(a), the Lellaraps might attack before you finish.

In order to finish the work faster, you hire a second translator whose skills in Lellarap are as good as yours. As shown in Figure 2.5(b), you divide the document into two nearly equal parts, Document A and Document B. You translate Document A, and your colleague translates Document B. When both translations are complete, you merge the two, check the translation, and send the result to the world’s leaders.

documentTranslationFigure
Figure 2.5 (a) Translation by one translator. Time ts gives the sequential time taken. (b) Translation by translators A and B. Times t1, t2, t3, and t4 give the times needed to do each component of the concurrent approach.

Translating the demands alone is a sequential approach. In this context, sequential mean non-parallel. Translating the demands with two people is a parallel approach. It’s concurrent as well because you have to decide how to split up the document and how to merge it back together.

If you wrote a computer program to translate the demands using the sequential approach, you’d produce a sequential program. If you wrote a computer program that uses the approach shown in Figure 2.5(b), it would be a concurrent program. A concurrent program is also referred to as a multi-threaded program. Threads are sequences of code that can execute independently and access each other’s memory. Imagine you’re one thread of execution and your colleague is another. Thus, the concurrent approach will have at least two threads. It may have more if separate threads are used to divide up the document or merge it back together.

Because we’re interested in the time the process takes, we’ve labeled different tasks in Figure 2.5 with their running times. We let ts be the time for one person to complete the translation. The times t1 through t4 mark the times needed to complete tasks 1 through 4, indicated in Figure 2.5(b).

2.5.3. Kinds of concurrency

A sequential program, like the single translator, uses a single processor on a multi-processor system or a single core on a multicore chip. To speed up the solution of a program on a multicore chip, it may be necessary to divide a problem so that different parts of it can be executed concurrently.

This process of dividing up a problem falls into the category of domain decomposition, task decomposition, or some combination of the two. In domain decomposition, we take a large amount of data or elements to be processed and divide up the data among workers that all do the same thing to different parts of the data. In task decomposition, each worker is assigned a different task that needs to be done. The following two examples explore each of these approaches.

Example 2.8 Domain decomposition

Suppose we have an autonomous robot called the Room Rating Robot or R3. The R3 can measure the area of any home. Suppose that we want to use an R3 to measure the area of the home with two floors sketched in Figure 2.6.

floorPlan
Figure 2.6 A home with two floors.

One way to measure the area is to put an R3 at the entrance of the home on the first floor and give it the following instructions:

  1. Initialize total area to 0

  2. Find area of next room and add to total area

  3. Repeat Step 2 until all rooms have been measured

  4. Move to second floor

  5. Repeat Step 2 until all rooms have been measured

  6. Display total area

By following these steps, the R3 will systematically go through each room, measure its area, and add the value to the total area found so far. It’ll then climb up the stairs and repeat the process for the second floor. It would add up the areas from the two floors and give us the total living area of the house. This is a sequential approach for measuring the area.

Now, suppose we have two R3 robots, named R3A and R3B. We can put R3A on the first floor and R3B on the second. Both robots are then instructed to find the area of the floor they’re on using steps very similar to the ones listed above for a sequential solution. When done, we add together the answers from R3A and R3B to get the total. This is a concurrent (and also parallel) approach for measuring the living area of a home with two floors. Using two robots this way could speed up the time it takes to measure the area.

In the example above, the tasks are the same (measuring the area) but are performed on two different input domains (the floors). This type of task division is also known as domain decomposition. Here, to achieve concurrency, we take the domain of the problem (the house) and divide it into smaller subdomains (its floors). Then, each processor (or robot in this example) performs the same task on each subdomain. When done, the final answer is found by combining the answers. Running the robots on each floor is purely parallel, but combining the answers is concurrent since some interaction between the robots is necessary.

Another way of solving a problem concurrently is to divide it into fundamentally different tasks. The tasks could be executed on different processors and perhaps on different input domains. Eventually, some coordination of the tasks must be done to generate the final result. The next example illustrates such a division.

Example 2.9 Task decomposition

Let’s expand the problem given in Example 2.8. R3 robots can do more than just measure area. In addition to calculating the living area of a home, we want an R3 robot to check if the electrical outlets are in working condition. The robot should give us the area of the house as well as a list of electrical outlets that are not working.

This problem can be solved in a sequential manner with just one robot. One way to do so would have a robot make a first pass through all floors and rooms and compute the living area. It could then make a second pass and compile a list of electrical outlets that are not working.

A way to solve this problem concurrently is to assign R3A to measure the area and R3B to identify broken electrical outlets. Once the respective tasks are assigned, we place the robots at the entrance to the house and activate them. It’s possible that the two robots will bump into each other while working, and that’s one of the difficulties of concurrency. The burden is on the programmer to give instructions so that the robots can avoid or recover from collisions. After the robots are done, we ask R3A for the living area of the house and R3B for a list of broken outlets.

2.6. Summary

In this chapter, we introduced an approach for developing software to solve problems with a computer. A number of examples illustrated how to move from a problem statement to a complete Java program. Although we have given rough explanations of the Java programs in this chapter, we encourage you to play with each program to expand its functionality. Several exercises prompt you to do just that. It’s impossible to learn to program without actively practicing programming. Never be afraid of “breaking” the program. Only by breaking it, changing it, and fixing it will your understanding grow.

In addition to the software development lifecycle, we introduced several building blocks of Java syntax including classes, main() methods, import statements, and variable declarations. We also gave a preview of different variable types (int, double, and String) and operations that can be used with them. Material about types and operations on them is covered in depth in the next chapter. Furthermore, we discussed input and output using Scanner and System.out.print() for the command line interface and JOptionPane methods for a GUI.

Finally, we introduced the notions of sequential and concurrent solutions to problems and clarified the subtle difference between parallelism and concurrency.

2.7. Exercises

Conceptual Problems

  1. When solving a problem using a computer, what problem is solved by the programmer and what problem is solved by the program written by the programmer? Are they the same?

  2. In Program 2.2, we declared all variables to be of type double. How would the program behave differently if we had declared all the variables with type int?

  3. What is the purpose of the statement Scanner in = new Scanner(System.in); in Program 2.1?

  4. Explain the difference between a declaration and an assignment statement.

  5. Is the following statement from Program 2.7 a declaration, an assignment, or a combination of the two?

    String response = JOptionPane.showInputDialog(null,
    	message, title, JOptionPane.QUESTION_MESSAGE);
  6. When would you prefer using the JOptionPane class for output over System.out.print()? When might you prefer System.out.print() to using JOptionPane?

  7. Review Program 2.7 and identify all the Java keywords used in it.

  8. Try to recompile Program 2.7 after removing the import statement at the top. Read and explain the error message generated by the compiler.

  9. Explain the difference between parallel and concurrent tasks. Give examples of tasks that are parallel but not concurrent, tasks that are concurrent but not parallel, and tasks that are both.

  10. Refer to Figure 2.5. Suppose that you and your colleague translate from English to Lellarapian at the rate of 200 words per hour. Suppose that the list of demands contains 10,000 words.

    1. Compute ts, the time for you to translate the entire document alone, assuming that, after translation, you perform a final check at the rate of 500 words per hour.

    2. Now assume that the task of splitting up the document and handing over the correct part to your colleague takes 15 minutes. Also, the task of receiving the translated document from your colleague and merging with the one you translated takes another 15 minutes. After merging the two documents, you do a final check for correctness at a rate of 500 words per hour. Calculate the total time to complete the translation using this concurrent approach. Let us refer to this time as tc.

    3. One way to calculate the speedup of a concurrent solution is to divide the sequential time by the concurrent time. In our case, the speedup is ts/tc. Using the values you’ve computed in (a) and (b), calculate the speedup.

    4. Suppose that you have a total of two colleagues willing to help you with the translation. Assuming that the three of you will perform the translation and that the times needed to split, merge, and check are unchanged, calculate the total time needed. Then, compute the speedup.

    5. Now suppose that there are an unlimited number of people willing and able to help you with the translation. Will the speedup keep on increasing as you add more translators? Explain your answer.

  11. In Example 2.8, what aspect of a multicore system do the robots represent?

  12. In Example 2.8, suppose that you have two R3 robots available. You’d like to use them to measure the living area of a single-floor home. Suggest how two robots could be programmed to work concurrently to measure the living area faster than one.

Programming Practice

  1. Write a program that prompts the user for three integers from the command line. Read each integer in using the nextInt() method of a Scanner object. Compute the sum of these values and print it to the screen using System.out.print() or System.out.println().

  2. Expand the program from Exercise 2.13 so that it finds the average of the three numbers instead of the sum. (Hint: Try dividing by 3.0 instead of 3 to get an average with a fractional part. Then, store the result in a variable of type double.)

  3. Rewrite your solution to Exercise 2.14 so that it uses a JOptionPane-based GUI instead of Scanner and System.out.print().

  4. Copy and paste Program 2.1 into the Java IDE you prefer. Compile and run it and make sure that the program executes as intended. Then, add statements to prompt the user for the color of the ball and read it in. Store the color in a String value. Add an output statement that mentions the color of the ball.

  5. Rewrite your solution to Exercise 2.16 so that it uses a JOptionPane-based GUI instead of Scanner and System.out.print().

  6. In Example 2.4, we assumed that the speed is given in miles per hour and the time in hours. Change Program 2.2 to compute the distance traveled by a moving object given its speed in miles per hour but time in seconds. You will need to perform a conversion from seconds to hours before you can find the distance.

  7. A program can use both a command line interface and a GUI to interact with a user. Write a program that uses the Scanner class to read a String value containing the user’s favorite food. Then display the name of the food using JOptionPane.

  8. Use the complete software development cycle to write a program that reads in the lengths of two legs of a right triangle and computes the length of its hypotenuse.

    1. Make sure you understand the problem. How can you apply the Pythagorean formula a2 + b2 = c2 to solve it?

    2. Design a solution by listing the steps you will need to take to read in the appropriate values, find the answer, and then output it.

    3. Implement the steps as a Java program.

    4. Test the solution with several known values. For example, a right triangle with legs of lengths 3 and 4 has a hypotenuse of length 5. Which values cause errors? How should your program react to those errors?

    5. Consider what other features your program should have. If your intended audience is children who are learning geometry, should your error handling be different from an audience of architects?

3. Primitive Types and Strings

Originality exists in every individual because each of us differs from the others. We are all primary numbers divisible only by ourselves.

— Jean Guitton

3.1. Problem: College cost calculator

Perhaps you’re a student. Perhaps you aren’t. In either case, you must be aware of the rapidly rising cost of a college education. The motivating problem for this chapter is to create a Java program that can estimate this cost, including room and board. It starts by reading a first name, a last name, the per-semester cost of tuition, the monthly cost of rent, and the monthly cost of food as input from a user. Many students take out loans for college. In fact, student debt has surpassed credit card debt in the United States. We can implement a feature to read in an interest rate and the number of years expected to pay off the loan.

After taking all this data as input, we want to calculate the yearly cost of such an education, the four year cost, the monthly loan payment, and the total cost of the loan over time. Furthermore, we want to output this information on the command line in an attractive way, customized with the user’s name. Below is a sample execution of this program.

Welcome to the College Cost Calculator!
Enter your first name:       Holden
Enter your last name:        Caulfield
Enter tuition per semester:  $17415
Enter rent per month:        $350
Enter food cost per month:   $400
Annual interest rate:        .0937
Years to pay back your loan: 10

College costs for Holden Caulfield
***************************************
Yearly cost:                 $43830.00
Four year cost:              $175320.00
Monthly loan payment:        $2256.14
Total loan cost:             $270736.5

Samples from a command line interface can be confusing because it’s difficult to see what’s output and what’s input. In this case, we have marked the input in bold so that it’s clear what the user enters. In this program, the names, the tuition, the rent, the food, the interest rate, and the years to pay back the loan are taken as input. Note that the dollar signs are not part of the input and are printed as a cue to the user and to give visual polish.

We hope you already have a good understanding of this problem, but there are a few mathematical details worth addressing. First, the yearly cost is twice the semester tuition plus 12 times the rent and food costs. The four year cost is simply four times the yearly cost. The monthly loan payment amount, however, requires a formula from financial mathematics. Let P be the amount of the loan (the principal). Let J be the monthly interest rate (the annual interest rate divided by 12). Let N be the number of months to pay back the loan (years of the loan times 12). Let M be the monthly payment we want to calculate, given by the following formula.

payment

If you use the concepts and syntax from the previous chapter carefully, you might be able to solve this problem without reading further. However, there’s a depth to the ideas of types and operations that we haven’t explored fully. Getting a program to work is not enough. Programmers must understand every detail and quirk of their tools to avoid potential bugs.

3.2. Concepts: Types

Every operation inside of a Java program manipulates data. Often, this data is stored in variables, which look similar to variables from mathematics.

Consider the mathematical equation x + 3 = 7. In it, x has the value 4, and it always will. You can set up another equation in which x has a different value, but it won’t change in this one. Java variables are different. They’re locations where you can store something. If you decide later that you want to change what you stored there, you can put something else into the same location, overwriting the old value.

In contrast to a variable is a literal. A literal is a concrete value that does not change, though it can be stored in a variable. Numbers like 4 or 2.71828183 are literals. We need to represent text and single characters in Java as well, and there are literals like "grapefruit segment" and 'X' that fill these roles.

In Java, both variables and literals have types. If you think of a variable as a box where you can hold something, its type is the shape of that box. The kinds of literals that can be stored in that box must have a matching shape. In the last chapter, we introduced the type int to store integer values and the type double to store floating-point values. Java is a strongly typed language, meaning that, if we declare a variable of type int, we can only put int values into it (with a few exceptions).

This idea of a type takes some getting used to. From a mathematical perspective 3 and 3.0 are identical. However, if you have an int variable in Java, you can store 3 into it, but you can’t store 3.0. The type of a value will never change, but you can convert a value from one type to an equivalent value in another type.

3.2.1. Types as sets and operations

Before we go any further, let’s look deeper at what a type really is. We can think of a type as a set of elements. For example, int represents a set of integer values (specifically, the integers between -2,147,483,648 and 2,147,483,647, inclusive). Consider the following declarations:

int x;
int y;

These declarations indicate that the variables named x and y can only contain integer values in the int range. Furthermore, a type only allows specific operations. In Java, the int type allows addition, subtraction, multiplication, division, and several other operations we’ll talk about in Section 3.3, but there’s no built-in operation to raise an int value to a power in Java. Let’s assume that x has type int. As we discussed in the previous chapter, the expression x + 2 performs addition between the variable represented by x and the literal 2. Some languages use the operator ^ to mean raising a number to a power. Following this notation, some beginning Java programmers are tempted to write x ^ 2 to compute x squared. The ^ operator does have a meaning in Java, but it doesn’t raise values to a power. Other combinations of operators are simply illegal, such as x # 2.

The idea of using types this way gives structure to a program. All operations are well-defined. For example, you know that adding two int values together will give you another int value. Java is a statically typed language. This means that it can analyze all the types you’re using in your program at compile-time and warn you if you’re doing something illegal. Consequently, you’ll get a lot of compiler warnings and errors, but you can be more confident that your program is correct if all the types make sense.

3.2.2. Primitive and reference types in Java

As shown in Figure 3.1(a), there are two kinds of types in Java: primitive types and reference types. Primitive types are like boxes that hold single, concrete values. The primitive types in Java are byte, short, int, long, float, double, boolean, and char. These are types provided by the designers of Java, and it’s not possible to create new ones. Each primitive type has a set of operators that are legal for it. For example, all the numerical types can be used with + and -. We’ll talk about these types and their operators in great detail in Section 3.3.

typesInJavaFigure
Figure 3.1 (a) The two categories of types in Java. The int primitive type (b), the boolean primitive type (c), the String reference type (d), and a possible Aircraft reference type (e) are represented as sets of items with operations.

Reference types work differently from primitive types. For one thing, a reference variable points at an object. This means that when you assign one reference variable to another, you aren’t getting a whole new copy of the object. Instead, you’re getting another arrow that points at the same object. The result is that performing an operation on one reference can effectively change another reference, if they’re both pointing at the same object.

Another difference is that reference variables (or simply references) do not have a large set of operators that work on them. Every variable can be used with the assignment (=) and the comparison (==) operators. Every variable can also be concatenated with a String by using the ` operator, but even if two objects represent numerical values, they can't be added together with the ` operator.

References should still be thought of as types defining a set of objects and operations that can be done with them. Instead of using operators, however, references use methods. You’ve seen methods such as System.out.print() and JOptionPane.showInputDialog() in the previous chapter. A method generally has a dot (.) before it and always has parentheses afterward. These parentheses contain the input parameters or arguments that you give to a method. Using operators on primitive types is convenient, but on the other hand, there is no limit to the number, kind, or complexity of methods that can be used on references.

Another important feature of reference types is that anyone can define them. So far, you’ve seen the reference types String and JOptionPane. As we’ll discuss later, String is an unusual reference type in that it is built deeply into the language. There are a few other types like this (such as Object), but most reference types could have been written by anyone, even you.

To create a new type, you write a class and define data and methods inside of it. If you wanted to define a type to hold airplanes, you might create the Airplane class and give it methods such as takeOff(), fly(), and land() because those are operations that any airplane should be able to do.

Once a class has been defined, it’s possible to instantiate an object. An object is a specific instance of the type. For example, the type might be Airplane, but the object might be referenced by a variable called sr71Blackbird. Presumably, this object has a weight, a maximum speed, and other characteristics that mark it as a Lockheed SR-71 “Blackbird,” the famous spy plane. To summarize: An object is a concrete instance of data. A reference is a variable that gives a name to (points to) an object. A type is a class that both the variable and the object have that defines what kinds of data the object contains and what operations it can perform.

The following table lists some of the differences between primitive types and reference types.

Primitive Types Reference Types

Created by the designers of Java

Created by any Java programmer

Use operators to perform operations

Use methods to perform operations

There are only eight different primitive types

The number of reference types is unlimited and grows every time someone creates a new class

Hold a specific numbers of bytes of data depending on the type

The referenced object can hold arbitrary amounts of data

Assignment copies a value from one place to another

Assignment copies an arrow that points at an object

Declaration creates a box to hold values

Declaration creates an arrow that can point at an object, but only instantiation creates a new object

3.2.3. Type safety

Why do we have types? There are weakly typed languages where you can store any value into almost any variable. Why bother with all these complicated rules? Most assembly languages have no notion of types and allow the programmer to manipulate memory directly.

Because Java is strongly typed, the type of every variable, whether primitive or reference, must be declared prior to its use. This constraint allows the Java compiler to perform many safety and sanity checks during compilation, and the JVM performs a few more during execution. These checks avoid errors during program execution that might otherwise be hard to find, errors that could lead to catastrophic failures of the program.

The Ariane 5 rocket is an example of a catastrophic failure due to a type error. On its first flight, the rocket left its flight path and eventually exploded. The failure was caused because of errors from converting a 64-bit floating-point to 16-bit signed integer value. The converted value was larger than the integer could hold, resulting in a meaningless value.

Converting from one type to another is called casting. The Ariane 5 failure was due to a problem with casting that was not caught. Even in Java, it’s possible for a human being to circumvent type safety with irresponsible casting.

3.3. Syntax: Types in Java

In this section we dig deeper into the type system in Java, starting with variables and moving on to the properties of the eight primitive types and the properties of String and other reference types.

3.3.1. Variables and literals

To use a variable in Java, you must first declare it, which sets aside memory to hold the variable and attaches a name to that space. Declarations always follows the same pattern. The type is written first followed by the identifier, or name, for the variable. Below we declare a variable named value of type int.

int value;

Note how we use the same pattern to declare a reference variable named creature of type Wombat.

Wombat creature;

You’re free to declare a variable and then end the line with a semicolon (;), but it’s common to initialize a variable at the same time. The following line simultaneously declares value and initializes it to 5.

int value = 5;
Pitfall: Multiple declarations

Don’t forget that you’re both declaring and initializing in a line like the above. Beginning Java programmers sometimes try to declare a variable more than once, as in the following:

int value = 5;
int value = 10;

Java won’t allow two variables with the same name to exist in the same block of code. The programmer probably intended the following, which reuses variable value and replaces its contents with 10.

int value = 5;
value = 10;

This error is more common when several other lines of code lie between the two assignments.

In some of the examples above, we’ve stored the value 5 into our variable value. The symbol 5 is an example of a literal. A literal is a value represented directly in code. It cannot be changed, but it can be stored into variables that have a matching type. The values stored into variables come from literals, input, or more complicated expressions. Just like variables, literals have types. The type of 5 is int while the type of 5.0 is double. Other types have literals written in ways we’ll discuss below.

3.3.2. Primitive types

The building blocks of all Java programs are primitive types. All objects must fundamentally contain primitive types deep down inside. There are eight primitive types. Half of them are used to represent integer values, and we’ll start by looking at those.

Integers: byte, short, int, and long

A variable intended to hold integer values can be declared with any of the four types byte, short, int, or long. All of them are signed (holding positive and negative numbers) and represent numbers in two’s complement. They only differ by the range of values that each type can hold. These ranges and the number of bytes used to represent variables from each type are given below.

Table 3.1 Ranges for primitive integer types in Java.
Type Bytes Range
byte

1

-128

to

127

short

2

-32,768

to

32,767

int

4

-2,147,483,648

to

2,147,483,647

long

8

-9,223,372,036,854,775,808

to

9,223,372,036,854,775,807

Note that the entire range of byte is included in that of short, of short in that of int, and so on. We say that short is broader than byte, int is broader than short, and long is broader than int.

A variable declared with type byte can only represent 256 different values, the integers in the range -128 to 127. Why use byte at all, then? Since a byte value only takes up a single byte, it can save memory, especially if you have a list of variables called an array, which we will discuss in Chapter 6. However, too narrow of a range will result in underflow and overflow. Java programmers are advised to stick with int for general use. If you need to represents values larger than 2 billion or smaller than -2 billion, use long. Once you’re an experienced programmer, you may occasionally use byte and short to save space, but they should be used sparingly and for a clear purpose.

Example 3.1 Integer variables

Consider the following declarations.

byte age;
int numberOfRooms;
long foreverCounter = 0;

The first of these statements declares age to be a variable of type byte. This declaration means that age can assume any value from the range for byte. For a human being, this limitation is reasonable (but dangerously close to the limit) since there is no documented case of a person living more than 122 years. Similarly, the next declaration declares numberOfRooms to be of type int. The last declaration declares foreverCounter to be of type long and initializes it to 0.

Since age is a variable, its value can change during program execution. Note that the above declaration of age does not assign a value to it. When they are declared, all integer variables are set to 0 by Java. However, to make sure that the programmer is explicit about what he or she wants, the compiler will give an error in most cases if a variable is used without first having its value set.

Like any other integer variable, we can assign age a value as follows.

age = 32;

Doing so assigns the value 32 to variable age. Note that the Java compiler would not complain if you were to assign -10 to the variable age, even though it’s impossible for a human to have a negative age (at least, without a time machine). Java attaches no meaning to the name you give to a variable.

Earlier, we said that variables had to match the type of literals you want to store into them. In the example above, we declared age with type byte and then stored 32 into it. What is the type of 32? Is it byte, short, int, or long? By default, all integer literals have type int, but they can be used with byte or short variables provided that they fit within the range. Thus, the following line causes an error.

byte fingers = 128;

If you want to specify a literal to have type long, you can append l or L to it. Thus, 42 is an int literal, but 42L is a long literal. You should always use the capital L since l can be difficult to distinguish from 1.

At the time of this writing, Java 11 is the newest version of Java, but Java 8 is most commonly used. In Java 7 and higher, you’re allowed to put any number of underscores (_) inside of numerical literals to break them up for the sake of readability. Thus, 123_45_6789 might represent a social security number, or you could use underscores instead of commas to write three million as 3_000_000. Since most compilers support Java 7 or higher now, it’s reasonable to use this underscore notation in your literals. Note that you should never use a comma in a numerical Java literal, no matter which version of Java you’re using.

Floating-point numbers: float and double

To represent numbers with fractional parts, Java provides two floating-point types, double and float. Because of limits on floating-point precision discussed in Chapter 1, Java cannot represent all real or rational numbers, but these types provide good approximations. If you have a variable that takes on floating-point values such as 3.14, 1.707 × 1025, 9.8, or similar, it ought to be declared as a double or a float.

Example 3.2 Floating-point declarations

Consider the following declarations.

float roomArea;
double avogadro = 6.02214179E23

The first of the above two statements declares roomArea to be of type float. Note that the declaration does not initialize roomArea to any value. Similar to integer primitive types, an uninitialized floating-point variable contains 0.0, but Java usually forces the programmer to assign a value to a variable before using it. The second of the above two statements declares avogadro to be a variable of type double and initializes it to the well-known Avogadro constant 6.02214179 × 1023. Note the use of E to mean “ten to the power of.” In Java, you could write 0.33 × 10-12 as 0.33E-12, or the number -4.325 × 1018 as -4.325E18 (or even -4.325E+18 if you’d like to write the sign of the exponent explicitly).

Accuracy in number representation

As discussed in Chapter 1, integer types within their specified ranges have exact representations. For example, if you assign 19 to a variable of type int and then print this value, you always get exactly 19. Floating-point numbers do not have this guarantee of exact representation.

Example 3.3 Floating-point accuracy

Try executing the following statements within a Java program.

double test = 0.0;
test += 0.1;
System.out.println(test);
test += 0.1;
System.out.println(test);
test += 0.1;
System.out.println(test);

Since we’re adding 0.1 each time, one would expect to see outputs of 0.1, 0.2, and 0.3. The first two numbers print as expected, but the third number prints out as 0.30000000000000004. It may seem counterintuitive, but 0.1 is a repeating decimal in binary, meaning that it cannot be represented exactly using the 64-bit IEEE floating-point standard. The System.out.println() method hides this ugliness by rounding the output past a certain level of precision, but by the third addition, the number has drifted far enough away from 0.3 that an unexpected number peeks out.

Variables of type float give you an accuracy of about 6 decimal digits while those of type double give about 15 decimal digits. Does the accuracy of floating-point number representation matter? The answer to this question depends on your application. In some applications, 6-digit accuracy may be adequate. However, when doing large-scale simulations, such as computing the trajectory of a spacecraft on a mission to Mars, 15-digit accuracy might be a matter of life or death. In fact, even double precision may not be enough. There is a special BigDecimal class which can perform arbitrarily high precision calculations, but due to its low speed and high complexity, it should only be used in those rare situations when a programmer requires a much higher level of precision than what double provides.

Java programmers are recommended to use double for general purpose computing. The float type should only be used in special cases where storage or speed are critical and accuracy is not. Because of its greater accuracy, double is considered a broader type than float. You can store float values in a double without losing precision, but the reverse is not true.

All floating-point literals in Java have type double unless they have an f or F appended on the end. Thus, 3.14 is a double literal, but 3.14f is a float literal.

Floating-point output

Formatting output for floating-point numbers has an extra complication compared with integer output: How many digits after the decimal point should be displayed? If you’re representing money, it’s common to show exactly two digits after the decimal point. By default, all of the non-zero digits are shown.

Instead of using System.out.print(), you can use System.out.format() to control formatting. When using System.out.format(), the first argument to the method is a format string, a piece of text that gives all the text you want to output as well as special format specifiers that indicate where other data is to appear and how it should be formatted. This method takes an additional argument for each format specifier you use. The specifier %d is for integer values, the specifier %f is for floating-point values (including both float and double types), and the specifier %s is for text. Consider the following example:

System.out.format("%s! I broke %d records in %f seconds.\n", "Bob", 3, 2.4985);

The output of this code is

Bob! I broke 3 records in 2.4985 seconds.

This kind of output is based on the printf() function used for output in the C programming language. It allows the programmer to have a holistic picture of what the final output might look like, but it also gives control of formatting through the format specifiers. For example, you can choose the number of digits for a floating-point value to display after the decimal point by putting a . and the number between the % and the f.

System.out.format("$%.2f\n", 123.456789 );

The output of this code is:

$123.46

Note that the last visible digit is rounded instead of truncated. Note that %n is a special format specifier that indicates a newline. To learn about other ways to use format strings to manipulate output, read the Oracle Formatter documentation.

Basic arithmetic

The following table lists the arithmetic operators available in Java. All of these operators can be used on both the integer primitive types and the floating-point primitive types.

Operator Meaning
+

Add

-

Subtract

*

Multiply

/

Divide

%

Modulus (remainder)

The first four of these should be familiar. Addition, subtraction, and multiplication work as you would expect, provided that the result is within the range defined for the types you’re using, but division is a little confusing. If you divide two integer values in Java, you’ll get an integer as a result. If there would have been a fractional part, it will be truncated, not rounded. Consider the following.

int x = 1999/1000;

In normal mathematics, 1,999 ÷ 1,000 = 1.999. In Java, 1999/1000 yields 1, and that’s what is stored in x. For floating-point numbers, Java works much more like normal mathematics.

double y = 1999.0/1000.0;

In this case, y contains 1.999. The literals 1999.0 and 1000.0 have type double. The type of y does not affect the division, but it had to be double to be a legal place to store the result.

Pitfall: Unexpected integer division

It’s easy to focus on the variable and forget about the types involved in the operation. Consider the following.

double z = 1999/1000;

Because z has type double, it seems that the result of the division should be 1.999. However, the dividend and the divisor have type int, and the result is 1. This value is converted into double and stored in z as 1.0. This mistake is more commonly seen in the following scenario.

double half = 1/2;

The code looks fine at first, but 1/2 yields 0. If the result is to be stored in a double variable, it’s better to multiply by 0.5 instead of dividing by 2.

You may not have thought about this idea since elementary school, but the division operator (/) finds the quotient of two numbers. The modulus operator (%) finds the remainder. For example, 15 / 6 is 2 while 15 % 6 is 3 because 6 goes into 15 twice with 3 left over. The modulus operator is usually used with integer values, but it’s also defined to work with floating-point values in Java. It’s easy to dismiss the modulus operator because we don’t often use it in daily life, but it’s incredibly useful in programming. On its face, it allows us to see the remainder after division. This idea can be applied to see if a number is even or odd. It can also be used to compress a large range of random integers to a smaller range or perform a kind of circular arithmetic useful for cryptography. Keep an eye out for it. We’ll use it many times in this book.

Precedence

Although all the previous examples use only one mathematical operator, you can combine several operators and operands into a larger expression like the following.

((a + b) * (c + d)) % e

Such expressions are evaluated from left to right, using the standard order of operations: The * and / (and also %) operators are given precedence over the + and - operators. Like in mathematics, parentheses have the highest precedence and can be used to add clarity. Thus, the order of evaluation of a + b / c is the same as a + (b / c) but different from (a + b) / c.

Example 3.4 Order of operations

Consider the following lines of code.

int a = 31;
int b = 16;
int c = 1;
int d = 2;
a = b + c * d - a / b / d;

What’s the result? The first operation to be evaluated is c * d, yielding 2. The next is a / b, yielding 1, which is then divided by d, yielding 0. Next, b + 2 gives 18, and 18 - 0 is still 18. Thus, the value stored in a is 18.

Your inner mathematician might be nervous that a is used in the expression on the right side of the assignment and is also the variable where the result is stored, but this situation is very common in programming. The value of a doesn’t change until after all the math has been done. The assignment always happens last.

All of the operators we’ve discussed so far are binary operators. This use of the word “binary” has nothing to do with base 2. A binary operator takes two things and does something, like adding them together. A unary operator takes a single operand and does something. The - operator can be used as a unary operator to negate a literal, variable, or expression. A unary negation has a higher precedence than the other operators, just like in mathematics. In other words, the variable or expression will be negated before it’s multiplied or divided. The + operator can be used anywhere you’d use a unary negation, although it doesn’t actually do anything. Consider the following statements.

int a = - 4;
int b = -c + d / -(e * f);
int s = +t + (-r);
Shortcuts

Some operations happen frequently in Java. For example, increasing a variable by some amount is a common task. If you want to increase the contents of variable value by 10, you can write the following.

value = value + 10;

Although the statement above is not excessively long, increasing a variable is common enough that there’s shorthand for it. To achieve the same effect, you can use the += operator.

value += 10;

The += operator gets the contents of the variable, in this case value, adds whatever is on its right side, in this case 10, and stores the result back into the variable. Essentially, it saves you from writing the name of the variable twice. And += is not the only shortcut. It’s only one member of a family of shortcut operators that perform a binary operation between the variable on the left side and the expression on the right side and then store the value back into the variable. There’s a -= operator that decreases a variable, a *= operator that scales a variable, and several others, including shortcuts for bitwise operations we cover in the next subsection.

Operator Example Meaning
+=
a += b;
a = a + b;
-=
a -= b;
a = a - b;
*=
a *= b;
a = a * b;
/=
a /= b;
a = a / b;
%=
a %= b;
a = a % b;
&=
a &= b;
a = a & b;
^=
a ^= b;
a = a ^ b;
|=
a |= b;
a = a | b;
<<=
a <<= b;
a = a << b;
>>=
a >>= b;
a = a >> b;
>>>=
a >>>= b;
a = a >>> b;

These assignment shortcuts are useful and can make a line shorter and easier to read.

Pitfall: Weak type checking with assignment shortcuts

Because you can lose precision, it’s not allowed to store a double value into an int variable. Thus, the following lines of code are illegal and will not compile.

int x = 0;
x = x + 0.1;

In this case, the check makes a lot of sense. If you could add 0.1 to 0 and then store that value into an int variable, the fractional part would be truncated, keeping 0 in the variable. However, this safeguard against lost precision is not done with assignment shortcuts. Even though we expect the following lines to be functionally identical to the previous ones, they will compile (but still do nothing).

int x = 0;
x += 0.1;

This kind of error can cause problems when the program expects the value of x to grow and eventually reach some level.

There are also two unary shortcuts. Incrementing a value by one and decrementing a value by one are such common operations that they get their own special operators, ++ and --.

Operator Example Meaning
++
a++;
a = a + 1;
--
a--;
a = a - 1;

Using either an increment or decrement changes the value of a variable. In all other cases, the use of an assignment operator is required to change a variable. Even in the binary shortcuts given before, the programmer is reminded that an assignment is occurring because the = symbol is present.

Both the increment and decrement operators come in prefix and postfix flavors. You can write the ++ (or the --) in front of the variable you’re changing or behind it.

int value = 5;
value++; // Now value is 6
++value; // Now value is 7
value--; // value is 6 again

When used in a line by itself, either flavor works exactly the same. However, the incremented (or decremented) variable can also be used as part of a larger expression. In a larger expression, the prefix form increments (or decrements) the variable before the value is used in the expression. Conversely, the postfix form gives back a copy of the original value, effectively incrementing (or decrementing) the variable after the value is used in the expression. Consider the following example.

int prefix = 7;
int prefixResult = 5 + ++prefix;

int postfix = 7;
int postfixResult = 5 + postfix++;

After the code is executed, the values of prefix and postfix are both 8. However, prefixResult is 13 while postfixResult is only 12. The original value of postfix, which is 7, is added to 5, and then the increment operation happens afterward.

Pitfall: Increment confusion

Incrementing a variable in Java is a very common operation. Expressions like i++ and ++i pop up so often that it’s easy to forget exactly what they mean. Programmers occasionally forget that they’re shorthand for i = i + 1 and begin to think of them as a fancy way to write i + 1.

When confused, a programmer might write something like the following.

int i = 14;
i = i++;

At first glance, it may appear that the second line of code really means i = i = i + 1. Assigning i an extra time is pointless, but it seems like it shouldn’t do any harm. Remember that the postfix version gives back a copy of the original value before it’s been incremented. In this case, i will be incremented, but then its original value will be stored back into itself. In the code given above, the final value of i is still 14.

In general it’s unwise to perform increment or decrement operations in the middle of larger expressions, and we advise against doing so. In some cases, code can be shortened by cleverly hiding an increment in the middle of some other expression. However, when reading back over the code, it always takes a moment to be sure that increment or decrement is doing exactly what it should. The additional confusion caused by this cleverness is not worth the line of code saved. Furthermore, the compiler will translate the operations into exactly the same bytecode, meaning that the shorter version is no more efficient than the longer version when executed.

Nevertheless, many programmers enjoy squeezing their code down to the smallest number of lines of code possible. You may have to read code that uses increments and decrements in clever (if obscure) ways, but you should always strive to make your own code as readable as possible.

Bitwise operators

In addition to normal mathematical operators, Java provides a set of bitwise operators corresponding to the operations we discussed in Chapter 1. These operators perform bitwise operations on integer values. The bitwise operators are &, |, ^, and ~ (which is unary). In addition, there are bitwise shift operators: << for signed left shift, >> for signed right shift, and >>> for unsigned right shift. There is no unsigned left shift operator in Java.

Operator Name Description
&

Bitwise AND

Combines two binary representations into a new representation which has a 1 in every position where both the original representations have a 1

|

Bitwise OR

Combines two binary representations into a new representation which has a 1 in every position where either of the original representations has a 1

^

Bitwise XOR

Combines two binary representations into a new representation which has a 1 in every position that the original representations have different values

~

Bitwise NOT

Takes a representation and creates a new representation in which every bit is flipped from 0 to 1 and 1 to 0

<<

Signed left shift

Moves all the bits the specified number of positions to the left, shifting 0s into the rightmost bits

>>

Signed right shift

Moves all the bits the specified number of positions to the right, padding the left with copies of the sign bit

>>>

Unsigned right shift

Moves all the bits the specified number of positions to the right, padding with 0s

When used with byte and short, all bitwise operators will automatically convert their operands to 32-bit int values. It’s crucial to remember this conversion since the number of bits used for representation is a fundamental part of bitwise operators.

The following example shows these operators in use. In order to understand the output, you need to understand how integers are represented in the binary number system, which is discussed in Section 1.3.

Example 3.5 Binary operators in Java

The following code shows a sequence of bitwise operations performed with the values 3 and -7. To understand the results, remember that, in 32-bit two’s complement representation, 3 = 0000 0000 0000 0000 0000 0000 0000 0011 and -7 = 1111 1111 1111 1111 1111 1111 1111 1001.

int x = 3;
int y = -7;
int z = x & y;
System.out.println("x & y\t= " + z);
z = x | y;
System.out.println("x | y\t= " + z);
z = x ^ y;
System.out.println("x ^ y\t= " + z);
z = x << 2;
System.out.println("x << 2\t= " + z);
z = y >> 2;
System.out.println("y >> 2\t= " + z);
z = y >>> 2;
System.out.println("y >>> 2\t= " + z);

The output of this fragment of code is:

x & y   = 1
x | y   = -5
x ^ y   = -6
x << 2  = 12
y >> 2  = -2
y >>> 2 = 1073741822

Note how the escape sequence \t is used to put a tab character in the output, making the results line up.

Why use the bitwise operators at all? Sometimes you may read data as individual byte values, and you might need to combine four of these values into a single int value. Although the signed left shift (<<) and signed right shift (>>) are, respectively, equivalent to repeated multiplications by 2 or repeated divisions by 2, they’re faster than doing these operations over and over. Finally, some of these operations are used for cryptographic or random number generation purposes.

Casting

Sometimes you need to use different types (like integers and floating-point values) together. Other times, you have a value in one type but need to store it in another (like when you’re rounding a double to the nearest int). Some combinations of operators and types are allowed, but others cause compiler errors.

The guiding rule is that Java allows an assignment from one type to another, provided that no precision is lost. That is, we can copy a value of one type into a variable of another type, provided that the destination variable has a broader type than the source value. The next few examples illustrate how to convert between different numerical types.

Example 3.6 Upcast with integers

Consider the following statements.

short x = 341;
int y = x;

Because the type of y is int, which is broader than short, it i’s legal to assign the value in x to variable y. In the assignment, a value with the narrower type short is converted to an equivalent value with the broader type int. Converting from a narrower type to a broader type is called an upcast or a promotion, and Java allows it with no complaint. Most languages allow upcasts without any special syntax because it’s always safe to move from a narrower, more restrictive type to a broader, less restrictive one.

Example 3.7 Downcast error

Consider these statements that declare variables a, b, and c and compute a value for c.

int a = 10;
int b = 2;
byte c;
c = a + b;

If you try compiling these statements as part of a Java program, you get an error message like the following.

Error: possible loss of precision
found: int
required: byte

The compiler generates the error above because the sum of two int values is another int value, which could be greater than the maximum value you can store in the byte variable c. In this example, you know that the value 12 doesn’t exceed the maximum of 127, but the Java compiler is inherently cautious. It complains whenever the type of the expression to be evaluated is broader than the type of the destination variable.

Example 3.8 Upcast from integers to floating-point

Integers are automatically converted to floating-point when needed. Consider the following statement.

double tolerance = 3;

The literal 3 has type int, but it’s automatically converted to the floating-point value 3.0 with type double. Again, double (and also float) are considered broader types than any integer types. Consequently, this type conversion is an upcast and is completely legal.

Upcasts also occur with arithmetic operations. Whenever you try to do arithmetic with two different numerical types, the narrower type is automatically upcast to the broader one.

double value = 3 + 7.2;

In this statement, 3 is automatically upcast to its double version 3.0 because 7.2 has the broader double type.

In order to perform a downcast, the programmer has to mark that he or she intends for the conversion to happen. A downcast is marked by putting the result type in parentheses before the expression you want converted. The next example illustrates how to cast a double value to type int.

Example 3.9 Downcast from double to int

The following statements cause a compiler error because an expression with type double cannot be stored into a variable with type int.

double roomArea = 3.5;
int houseArea = roomArea * 4.0;

A downcast can lose precision, and that’s why Java doesn’t allow it. Since a downcast is sometimes necessary, you can override Java’s type system with an explicit cast. To do so, we put the expected (or desired) result type in parentheses before the expression. In this case and many others, it’s also necessary to surround the expression with parentheses so that the entire expression (and not just roomArea) is converted to type int.

double roomArea = 3.5;
int houseArea = (int) (roomArea * 4.0);

In this case, the expression has value 14.0. Consequently, the int version is 14. In general, the value could have a fractional part. When casting from a floating-point type to an integer type, the fractional part is truncated not rounded. Consider the following statement:

int count = (int) 15.99999;

Mathematically, it seems obvious that 15.99999 should be rounded to the nearest int value of 16, but Java does not do this. Instead, the code above stores 15 into count. If you want to round the value, Java provides a method for rounding in the Math class. The rounding (instead of truncating) version is given below.

int count = (int) Math.round(15.99999);

The value given back by Math.round() has type long. The designers of the Math class chose long so that the same method could be used to round large double values into a long value, since the result might not fit in an int value. Since long is a broader type than int, we have to downcast the result to an int so that we can store it in count.

Example 3.10 Conversion from double to float

Consider the following declaration and assignment of variable roomArea.

float roomArea;
roomArea = 2.0;

This assignment is illegal in Java, and the compiler gives an error message like the following.

Error: possible loss of precision
found: double
required: float

As we mentioned earlier, the literal 2.0 has type double. When you try to assign a double value to a float variable, there’s always a risk that precision will be lost. The best way to avoid the error above is to declare roomArea with type double. Alternatively, we could store the float literal 2.0f into roomArea. We could also assign 2 instead of 2.0 to roomArea, since the upcast from int is done automatically.

Remember, you should almost always use the double type to represent floating-point numbers. Only in rare cases when you need to save memory should you use float values. By making it illegal to store 2.0 into a float variable, Java’s encouraging you to use high precision storage.

Numerical types and the conversions between them are critical elements of programming in Java, which has a strong mathematical foundation. In addition to these numerical types, Java also provides two other types that represent individual characters and Boolean values. We examine these next.

Characters: char

Sentences are made up of words. Words are made up of letters. Although we have discussed powerful tools for representing numbers in Java, we need a way to represent the letters and other characters we might find in printed text. Values with the char type are used to represent individual characters.

In the older languages of C and C++, the char type used 8 bits for storage. From Chapter 1, you know that you can represent up to 28 = 256 values with 8 bits. The Latin alphabet, which is used to write English, uses 26 letters. If we need to represent upper- and lowercase letters, the 10 decimal digits, punctuation marks, and quite a few other special symbols, 256 values is plenty. However, people all over the world use computers and want to store text from their language written in their script digitally. Taking the Chinese character system alone, some Chinese dictionaries list over 100,000 characters!

Java uses a standard called UTF-16 encoding to represent characters. UTF-16 is part of a larger international standard called Unicode, which is an attempt to represent most of the world’s writing systems as numbers that can be stored digitally. Most of the inner workings of Unicode aren’t important for day-to-day Java programming, but you can visit the Unicode site if you want more information.

In Java, each variable of type char uses 16 bits of storage. Therefore, each character variable could assume any value from among a total of 216 = 65,536 possibilities (although a few of these are not legal characters). Here are a few declarations and assignments of variables of type char.

char letter = 'A';
char punctuation = '?';
char digit = '7';

We’re storing char literals into each of the variables above. Most of the char literals you’ll use commonly are made by typing the single character you want in single quotes ('), such a 'z'. These characters can be upper- or lowercase letters, single numerical digits, or other symbols.

The space character literal is ' ', but some characters are harder to represent. For example, a new line (the equivalent of pressing <enter>) is represented as a single character, but we can’t type a single quote, hit <enter>, and then type the second quote. Instead, the character to represent a new line is '\n', which we will refer to simply as a newline. Every char variable can only hold a single character. It appears that '\n' has multiple characters in it, but it doesn’t. The use of the backslash (\) marks an escape sequence, which is a combination of characters used to represent a specific difficult to type or represent character. Here is a table of common escape sequences.

Escape Sequence Character
\n

Newline

\t

Tab

\'

Single quote

\\

Backslash

Remember, everything inside of a computer is represented with numbers, and each char value has some numerical equivalent. These numbers are arbitrary but systematic. For example, the character 'a' has a numerical value of 97, and 'b' has a numerical value of 98. The codes for all of the lowercase Latin letters are sequential in alphabetical order. (The codes for uppercase letters are sequential too, but there’s a gap between them and the lowercase codes.)

Some Unicode characters are difficult to type because your keyboard or operating system has no easy way to produce the character. Another kind of escape sequence allows you to specify any character by its Unicode value. There are large tables listing all possible Unicode characters by numerical values. If you want to represent a specific literal, you type '\uxxxx' where xxxx is a hexadecimal number representing the value. For example, '\u0064' converted into decimal is 16 × 6 + 4 = 100, which is the letter 'd'.

Example 3.11 Printing single characters

If you print a char variable or literal directly, it prints the character representation on the screen. For example, the following statement prints A not 65, the Unicode value of 'A'.

System.out.println('A');

However, the Unicode values are numbers. If you try to perform arithmetic on them, Java will treat them like numbers. For example, the following statement adds the integer equivalents of the characters (65 + 66 = 131), concatenates the sum with the String "C", and concatenates the result with a String representation of the int literal 999. The surprising final output is 131C999.

System.out.println('A' + 'B' + "C" + 999);
Booleans: boolean

If you’re new to programming, it may seem useless to have a type designed to hold only true and false values. These values are called Boolean values, and the logic used to manipulate them turns out to be crucial to almost every program. We use them to represent conditions in Chapter 4, Chapter 5, and beyond.

To store these truth values, Java uses the type boolean. There are exactly two literals for type boolean: true and false. Here are two declarations and assignments of boolean variables.

boolean awesome = true;
boolean testFailed = false;

If we could only store these two literals, boolean variables would have limited usefulness. However, Java provides a full range of relational operators that allow us to compare values. Each of these operators generates a boolean result. For example, we can test to see if two numbers are equal, and the answer is either true or false. All Java relational operators are listed in the table below. Assume that all variables used in the Example column have a numeric type.

Symbol Read as Example
==

equal to

x + 3 == y * 2
!=

not equal to

x !=  y / 4
<

less than

x < 3.5
<=

less than or equal to

x <= y
>

greater than

x > y+1
>=

greater than or equal to

x + y >= z
Example 3.12 Boolean variables

The following declarations and assignments illustrate some uses of boolean variables. Note the use of the relational operators == and >.

int x = 3;
int y = 4;
boolean same = (x == 3);
same = (x == y);
boolean xIsGreater = (x > y);

In the first use of == above, the value of same is true because the value of x is 3. In the second comparison, the value of same is false because the values of x and y are different. The value of xIsGreater is also false since the value of x is not greater than the value of y. All of the parentheses in this example are unnecessary and are used only for clarity.

In addition to the relational operators, Java also provides logical operators that can be used to combine or negate boolean values. These are the logical AND (&&), logical OR (||), logical XOR (^), and logical NOT (!) operators.

Name Operator Description

AND

&&

Returns true if both values are true

OR

||

Returns true if either value is true

XOR

^

Returns true if values are different

NOT

!

Returns the opposite of the value

All of these operators, except for NOT, are binary operators. Logical AND is used when you want your result to be true only if both the operands being combined evaluate to true. Logical OR is used when you want your result to be true if either operand is true. Logical XOR is used when you want your result to be true if one but not both of your operands is true. The unary logical NOT operator (!) results in the opposite value of its operand, switching true to false or false to true. Both the relational operators and the logical operators are described in greater detail in Chapter 4.

3.3.3. Reference types

Now we’ll move on to reference types, which vastly outnumber the primitive types, with new types created all the time. Nevertheless, the primitive types in Java are important, partly because they are the building blocks for reference types.

Recall that a variable with a reference type does not contain a concrete value like a primitive variable. Instead, the value it holds is a reference or arrow pointing to the “real” object. It’s like a name for an object. When you declare a reference variable in Java, it doesn’t initially point at anything, and you’ll get a compiler error if you try to use its value. For example, the following code creates a Wombat variable called w, but it doesn’t yet point at anything.

Wombat w;

To create an object in Java, you use the new keyword followed by the name of the type and parentheses, which can either be empty or contain data you want to use to initialize the object. This process is called invoking the constructor, which creates space for the object and then initializes it with the values you specify or with default values if you leave the parentheses empty. Below, we invoke the default Wombat constructor and point the variable w at the resulting object.

w = new Wombat();

Alternatively, a Wombat constructor might allow you to specify its mass in kilograms when creating one, as follows.

w = new Wombat(26.3);

Assignment of reference types points the two references to the same object. Thus, we can have two different Wombat references pointing at the same object.

Wombat w1 = new Wombat(26.3);
Wombat w2 = w1;
wombat
Figure 3.2 Two Wombat references pointing at the same object.

Then, anything we do to w1 will affect w2 and vice versa. For example, we can tell w1 to eat leaves using the eatLeaves() method.

w1.eatLeaves();

Perhaps this will increase the mass of the object that w1 points at to 26.9 kilograms. But the mass of the object that w2 points at will be increased as well, because they are the same object. Since primitive variables hold values and not references to objects, this kind of code works very differently with them. Consider the following.

int a = 10;
int b = a;
a = a + 5;

In this code, a is initialized to have a value of 10, and b is initialized to have whatever value a has, namely 10. The third line increases the value of a to 15, but b remains at 10.

primitive
Figure 3.3 Because they’re primitive, int variables store values, not references.

Now that we’ve highlighted some of the differences between primitive and reference types, we explain the String type more deeply. You use it frequently, but it has a few unusual features that are not shared by other reference types.

String basics

The String type is used to represent text in Java. A String object contains a sequence of zero or more char values. Unlike every other reference type, there is a literal form for String objects. These literals are written with the text you want to represent inside of double quotes ("), such as "Fight the power!". You can declare a String reference and initialize it by setting it equal to another String reference or a String literal. Like any other reference, you could leave it uninitialized.

There’s a difference between an uninitialized String (a reference that points to null) and a String of length 0. A String of length 0 is also known as an empty string and is written "". The space character (' ') and escape sequences such as '\n' can also be parts of a String and add to its length. For example, "ABC" contains three characters, but the String "A B C" has five, because the spaces on each side of 'B' count. The next example illustrates some ways of defining and using the String type.

Example 3.13 String assignment

The following declarations define two String references named greeting and title and initialize each with a literal.

String greeting = "Bonjour!"
String title = "French Greeting";

As you’ve seen in Chapter 2, we can output String values using System.out.print() and JOptionPane methods.

System.out.println(greeting);
JOptionPane.showMessageDialog(null, greeting, title, JOptionPane.INFORMATION_MESSAGE);

The first statement above displays Bonjour! on the terminal. The second statement creates a dialog box with the title French Greeting and the message Bonjour!

String operations

In Chapter 2, you saw that we can concatenate two String objects into a third String object using the + operator. This operator is unusual for a reference type. Almost all other reference types are only able to use the assignment operator (=) and the comparison operator (==). Like other reference types, the String class provides methods for interaction. We introduce a few String methods in this section and subsequent sections, but the String class defines many more.

Example 3.14 String concatenation

Here’s another example of combining String objects using the + operator.

String argument = "the cannon";
String phrase = "No argument but " + argument + "!";

In these statements, we initialize argument to "the cannon". We then compute the value of phrase by adding, or concatenating, three String values: "No argument but ", the value of argument, and "!". The result is "No argument but the cannon!". If argument had been initialized to "a pie in the face", then phrase would instead point to "No argument but a pie in the face!".

Another way of concatenating two String objects is by using the String concat() method.

String argument = "the cannon";
String exclamation = "!";
String phraseStart = "No argument but ";
String phrase = phraseStart.concat(argument);
phrase = phrase.concat(exclamation);

This sequence of statements gives the same result as the one above using the ` operator. In practice, the `concat()` method is rarely used because the ` operator is so convenient. Note that String objects in Java are immutable, meaning that calling a method on a String object will never change it. In the code above, calling concat() creates new String objects. The phrase reference points first at one String and then at a new String on the next line. In this case the reference can be changed, but a String object never changes once it’s been created. This distinction is a subtle but important one.

A host of other methods can be used on a String just like concat(). For example, the length of a String can be found using the length() method. The following statements prints 30 to the terminal.

String motto = "Fight for your right to party!";
System.out.println(motto.length()):

String literals are String objects as well, and you can call methods on them. The following code stores 11 into letters.

int letters = "cellar door".length();

Remember that a String is a sequence of char values. If you want to find out what char value sits at a particular location within a String, you can use the charAt() method.

This method is called with an int value giving the index you want to know about. Indexes inside of a String start at 0, not at 1. Zero-based numbering is used extensively in programming, and we discuss it further in Chapter 6. It may help if you think of the index as the number of characters that appear before the character at the specified index. The next example shows how charAt() can be used.

Example 3.15 Examining the char value at an index

To see what char is at a given location, we call charAt() with the index in question, as shown below.

String word = "antidisestablishmentarianism";
char letter = word.charAt(11);

In this case, letter is assigned the value 'b'. Remember, indexes for char values inside of a String start with 0. Thus, the char at index 0 is 'a', the char at index 1 is 'n', the char at index 2 is 't', and so on. If you count up to the twelfth char (which has index 11), it should be 'b'.

Every char inside of a String counts, whether it’s a letter, a digit, a space, punctuation, or some other symbol.

String text = "^_^ l337 #haxor# skillz!";
System.out.println(text.charAt(10));

This code prints out h since 'h' is the eleventh char (with index 10) in text.

A contiguous sequence of characters inside of a String is called a substring. For example, a few substrings of "Throw your hands in the air!" are "T", "Throw", "hands", and "ur ha". Note that "Ty" is not a substring because these characters don’t appear next to each other.

The next example shows how to use the substring() method to retrieve a substring from an existing String.

Example 3.16 Retrieving a substring

You can generate a substring of a String (which is, itself, a String) using the substring() method. The substring() method takes two arguments: the index where the substring starts and the index just after it ends, as shown in the following code.

String description = "slovenly";
String emotion = description.substring(1,5);
System.out.println(emotion);

This snippet of code prints love, since those are the characters at indexes 1 through 4 of "slovenly". Remember that String indexes are always zero-based. Also, the second argument of substring() is the index after the last one you want in your substring. Although this behavior is confusing, it’s a common design in many different string libraries in many different languages. One way to think about it is that the length of the substring is the second parameter minus the first. In this case, 5 - 1 = 4, the length of "love".

The String class also provides the indexOf() method to find the position of a substring, as shown in the next example.

Example 3.17 String search

Suppose we wish to find a String inside of another String. To do so, we call the indexOf() method on the String we’re searching inside of, with the String we’re searching for as the argument.

String countries = "USA Mexico China Canada";
String search = "China";
System.out.println(countries.indexOf(search));

The indexOf() method returns an int value that gives the position of the String we’re searching for. In the code above, the output is 11 because "China" appears starting at index 11 inside the countries String. Another way to think about it is that there are 11 characters before "China" in countries. If the given substring cannot be found, the indexOf() method returns -1. For example, -1 will be printed to the terminal if we replace the print statement above with the following.

System.out.println(countries.indexOf("Honduras"));

There are several other methods provided by String that we introduce as the need arises. If you are curious, you should look into the Java documentation for String in the Oracle String documentation for a complete list of available methods.

3.3.4. Assignment and comparison

Both assigning one variable to another and testing two variables to see if they’re equal to each other are important operations in Java. These operations are used on both primitive and reference types, but there are subtle differences between the two that we discuss below.

Assignment statements

Assignment is the act of setting one variable to the value of another. With a primitive type, the value held inside one variable is copied to the other. With a reference type, the arrow that points at the object is copied. All types in Java perform assignment with the assignment operator (=).

As we’ve discussed, values can be computed and then assigned to variables as in the following statement.

int data = Integer.parseInt(response);

In Java, a statement that computes a value and assigns it is called an assignment statement. The generic form of the assignment statement is as follows.

identifier = expression;

Here, identifier gives the name of some variable. For example, in the statement above, data is the name of the variable.

The right-hand side of an assignment statement is an expression that returns a value that’s assigned to the variable on the left-hand side. Even an assignment statement can be considered an expression, allowing us to stack multiple assignments into one line, as in the following code.

int a, b, c;
a = b = c = 15;

The Java compiler checks for type compatibility between the left and the right sides of an assignment statement. If the right-hand side is a broader type than the left-hand side (or is completely mismatched), the compiler gives an error, as in the following cases.

int number = 4.9;
String text = 9;
Comparison

Comparing two values to see if they’re the same uses the comparison operator (==) in Java. With primitive types, this kind of check is intuitive: The result is true if the two values are the same. With reference types, the value held by the variable is the arrow pointing to the object. Two reference variables could point to different objects with identical contents and return false when compared to each other. The following gives examples of these comparisons.

Example 3.18 Comparison

Consider the following lines of code.

int x = 5;
int y = 2 + 3;
boolean z = (x == y);

The value of variable z is true because x and y contain the same values. If x were assigned 6 instead, z would be false.

Now, consider the following code:

String thing1 = new String("Magical mystery");
String thing2 = new String("Magical mystery");
String thing3 = new String("Tragical tapestry");
differentObjectsFigure
Figure 3.4 Objects thing1, thing2, and thing3 (a) in their initial states and (b) after the assignment thing1 = thing2;.

This code declares and initializes three String values. Although it’s possible to store String literals directly without invoking a String constructor, we use this style of String creation to make our point since Java can do some confusing optimizations otherwise. Variables thing1 and thing2 point to String values that contain identical sequences of characters. Variable thing3 points to a different String. Consider the following statement.

boolean same = (thing1 == thing3);

In this case the value of same is clearly false because the two String values are not the same. What about the following case?

boolean same = (thing1 == thing2);

Again, same contains false. Although, thing1 and thing2 point at identical objects, they point at different identical objects. Since the value held by a reference is the arrow that points to the object, the comparison operator only shows that two references are the same if they point at the same object.

To better understand comparison between reference types, consider Figure 3.4(a), which shows three different objects. Note that each reference points at a distinct object, even though two objects have the same contents.

Now consider the following assignment.

thing1 = thing2;

As shown in Figure 3.4(b), this assignment points reference thing1 to the same location as reference thing2. Then, (thing1 == thing2) would be true.

The == operator is generally not very useful with references, and the equals() method should be used instead. This method compares the contents of objects in whatever way the designer of the type specifies. For example:

thing1.equals(thing2)

This statement is true when thing1 and thing2 are pointing at identical String objects even if they’re different objects.

3.3.5. Constants

In addition to normal variables, we can define named constants. A named constant is similar to a variable of the same type except that its value cannot be changed once set. A constant in Java is declared like any other variable with the addition of the keyword final before the declaration.

The convention in Java (and many other languages) is to name constants with all capital letters. Because camel case can no longer be used to tell where one word starts and another ends, an underscore (_) is used to separate words. Here are a few examples of named constant declarations.

final int POPULATION = 25000;
final double PLANCK_CONSTANT = 6.626E-34;
final boolean FLAG = false;
final char FIRST_INITIAL = 'A';
final String MESSAGE = "All your base are belong to us.";

In this code, the value of POPULATION is 25000 and cannot be changed. For example, if you now write POPULATION = 30000; on a later line, your compiler will give an error. PLANCK_CONSTANT, FLAG, FIRST_INITIAL, and MESSAGE are also defined as named constants. Because of the syntax Java uses, these constants are sometimes referred to as final variables.

In the case of MESSAGE and all other reference variables, being final means that the reference can never point at a different object. Even with a final reference, the objects themselves can change if their methods allow it. However, String objects can never change since they’re immutable.

Named constants are useful in two ways. First, a well-named constant can make your code more readable than using a literal. Second, if you do need to change the value to a different constant, you only have to change it in one place. For example, if you have used 25000 in five different places in your program, changing it to 30000 requires five changes. If you have used POPULATION throughout your program instead of a literal, you only have to change its value once.

3.4. Syntax: Useful libraries

Computer software is difficult to write, but many of the same problems come up over and over. If we had to solve these problems every time we wrote a program, we’d never get anywhere. Java allows us to use code other people have written called libraries. One selling point of Java is its large standard library that can be used by any Java programmer without special downloads. You’ve already used the Scanner class, the Math class, and perhaps the JOptionPane class, which are all part of libraries. Below, we’ll go deeper into the Math class and a few other useful libraries.

3.4.1. The Math library

Basic arithmetic operators are useful, but Java also provides a rich set of mathematical methods through the Math class. The following table lists a few of the methods available. For a complete list of methods provided by the Math class at the time of writing, see the Oracle Math documentation.

Table 3.2 A sample of methods available in the Java Math class. Arguments to trigonometric methods are given in radians.
Method Sample use Purpose

Trigonometric functions

cos()
double adjacent = hypotenuse * Math.cos(theta);

Find the cosine of the argument.

sin()
double opposite = hypotenuse * Math.sin(theta);

Find the sine of the argument.

tan()
double opposite = adjacent * Math.tan(theta);

Find the tangent of the argument.

Exponentiation and logarithms

exp()
double population = 250 * Math.exp(0.03 * time);

Compute ex, where x is the argument.

log()
double digits = Math.log(1000000);

Compute the natural logarithm of the argument.

pow()
double money = principal * Math.pow(1.0 + rate, time);

Compute ab, where a and b are the first and second arguments.

Miscellaneous

random()
double percent = Math.random();

Generate a random number x where 0.0 ≤ x < 1.0.

round()
long items = Math.round(material);

Round to the nearest long (or nearest int when rounding a float).

sqrt()
double hypotenuse = Math.sqrt(a*a+b*b);

Compute the square root of the argument.

Example 3.19 Math library usage

Here’s a program that uses the Math.pow() method to compute compound interest. Unlike Scanner and JOptionPane, the Math class is imported by default in Java programs and requires no explicit import statement.

Program 3.1 Computes interest earned and new balance.
import java.util.*;

class CompoundInterestCalculator {   
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);        
        System.out.println("Compound Interest Calculator");
        System.out.println();
        System.out.print("Enter starting balance: ");
        double startingBalance = in.nextDouble();
        System.out.print("Enter interest rate: ");
        double rate = in.nextDouble();
        System.out.print("Enter time in years: ");
        double years = in.nextDouble();        
        System.out.print("Enter compounding frequency: ");
        double frequency = in.nextDouble();
        double newBalance = startingBalance * 
            Math.pow(1.0 + rate/frequency, frequency*years);
        double interest = newBalance - startingBalance;
        System.out.println("Interest earned: $" + interest);
        System.out.println("New balance: $" + newBalance);
    }
}

In addition to methods, the Math library contains named constants including Euler’s number e and Ï€. These are written in code as Math.E and Math.PI, respectively. For example, the following assignment statement computes the circumference of a circle with radius given by the variable radius, using the formula 2Ï€r.

double circumference = 2*Math.PI*radius;

3.4.2. Random numbers

Random numbers are often needed in applications such as games and scientific simulations. For example, card games require a random distribution of cards. To simulate a deck of 52 cards, we could associate an integer from 1 to 52 with each card. If we had a list of these values, we could swap each value in the list with a value at a random location later in the list. Doing so is equivalent to shuffling the deck.

Java provides the Random class in package java.util to generate random values. Before you can generate a random number with this class, you need to create a Random object as follows.

Random random = new Random();

Here we’ve created an object named random of type Random. Depending on the kind of random value you need, you can use the nextInt(), nextBoolean(), or nextDouble() to generate a random value of the corresponding type.

// Random integer with all values possible
int balance = random.nextInt();

// Random integer between 0 (inclusive) and 130 (exclusive)
int humanAge = random.nextInt(130);

// Random boolean value
 boolean gender = random.nextBoolean();

// Random floating-point value between 0.0 (inclusive) and 1.0 (exclusive)
double percent = random.nextDouble();

In these examples, inclusive means that the number could be generated, while exclusive means that the number cannot be. Thus, the call random.nextInt(130) generates the integers 0 through 129 but never 130. Exclusive upper bounds on ranges of random values are very common in programming.

To generate a random int between values a and b, not including b, use the following code, assuming you have a Random object named random.

int count = random.nextInt(b - a) + a;

The nextInt() method call generates a value between 0 and b - a, and adding a shifts it into the range from a up to (but not including) b.

Generating a random double between values a and b is similar except that nextDouble() always generates a value between 0.0 and 1.0, not including 1.0. Thus, you must scale the output by b - a as shown below.

double value = random.nextDouble()*(b - a) + a;

The following example illustrates a potential use of random numbers in a video game.

Example 3.20 Dragon attribute generation

Suppose you’re designing a video in which the hero must fight a dragon with random attributes. Program 3.2 generates random values for the age, height, gender, and hit points of the dragon.

Program 3.2 Sets attributes of a randomly generated dragon for a video game.
import java.util.*; (1)

public class DragonAttributes {    
    public static void main(String[] args) {
        Random random = new Random(); (2)
        int age = random.nextInt(100) + 1; (3)
        double height = random.nextDouble()*30; (4)
        boolean gender = random.nextBoolean(); (5)
        int hitPoints = random.nextInt(51) + 25; (6)
        System.out.println("Dragon Statistics");
        System.out.println("Age:\t\t" + age);
        System.out.format("Height:\t\t%.1f feet\n", height);
        System.out.println("Female:\t\t" + gender);
        System.out.println("Hit points:\t" + hitPoints);
    }
}
1 We begin by importing java.util.* to include all the classes in the java.util package, including Random.
2 Then, we create an object random of type Random.
3 We use random to generate a random int between 0 and 99, to which we add 1, making an age between 1 and 100.
4 To generate the height, we multiply a random double by 75, yielding a value between 0.0 and 75.0 (exclusive).
5 Since there are only two choices for a dragon’s gender, we generate a random boolean value, interpreting true as female and false as male.
6 Finally, we determine the number of hit points the dragon has by generating a random int between 0 and 50, then add 25 to it, yielding a value between 25 and 75.

Because we are using random values, the output of Program 3.2 changes every time we run the program. Sample output is given below.

Dragon Statistics
Age:            90
Height:         13.7 feet
Female:         true
Hit points:     67

If you only need a random double value, you can generate a number between 0.0 and 1.0 (exclusive) using the Math.random() method from the Math class. This method is a quick and dirty way to generate random numbers without importing java.util.Random or creating a Random object.

The random numbers generated by the Random class and by Math.random() are pseudorandom numbers, meaning that they’re generated by a mathematical formula instead of truly random events. Each number is computed using the previous one, and the starting number is determined using time information from the OS. For most purposes, these pseudorandom numbers are good enough. Since each number can be predicted from the previous one, pseudorandom numbers are insufficient for some security applications. For those cases, Java provides the SecureRandom class which is slower than Random but produces random numbers that are much harder to predict.

3.4.3. Wrapper classes

Reference types have methods that allow a user to interact with them in many useful ways. The primitive types (byte, short, int, long, float, double, char, and boolean) do not have methods, but we sometimes need to manipulate them with methods or store them in a place that requires a reference type.

To deal with such situations, Java uses wrapper classes, reference types that correspond to each primitive type. Following Java conventions for class names, the wrapper types all start with an uppercase letter but are otherwise similar to the name of the primitive type they support: Byte, Short, Integer, Long, Float, Double, Character, and Boolean.

String to numerical conversions

A common task for a wrapper class is to convert a String representation of a number such as "37" or "2.097" to its corresponding numeric value. We had such a situation in Program 2.3, where we did the conversion as follows.

String response = JOptionPane.showInputDialog(null,
	"Enter the height: ", title, JOptionPane.QUESTION_MESSAGE);
height = Double.parseDouble(response);

This code uses the JOptionPane.showInputDialog() method to read from the user the height at which a ball is dropped. This method always returns data as a String. In order for us to do computation with the value, we need to convert it to a numeric type, such as an int or a double. To do so, we use the appropriate Byte.parseByte(), Short.parseShort(), Integer.parseInt(), Long.parseLong(), Float.parseFloat(), or Double.parseDouble() method.

The following example shows conversions from a String to a number using three of these methods.

Example 3.21 String to numeric conversion

Consider the following statements that show how a string can be converted to a numerical value.

String text = "15";
int count = Integer.parseInt(text);
float value = Float.parseFloat(text);
double tolerance = Double.parseDouble(text);

In this example, we declare a String object named text and initialize it to "15". Since text is a String and not a number, arithmetic expressions such as (text*29) are illegal.

To use the String "15" in a numerical computation, we need to convert it to a number. We used the Integer.parseInt(), Float.parseFloat(), and Double.parseDouble() methods to convert the String to int, float, and double values, respectively. Each method gives us 15 stored as the appropriate type.

What happens if the String "15.5" (or even "cinnamon") is given as input to the Integer.parseInt() method? If the String is not formatted as the appropriate kind of number, Java throws a NumberFormatException, probably crashing the program. An exception is an error or other unexpected situation that happens in the middle of running a program. We discuss how to work with exceptions in Chapter 12.

Character methods

When working with char values, it can be useful to know whether a particular value is a digit, a letter, or has a particular case. It may also be useful to convert a char to upper- or lowercase. Here is a partial list of the methods provided by the Character wrapper class to do these tasks.

Method Purpose
isDigit(char value)

Returns true if value is a numerical digit and false otherwise.

isLetter(char value)

Returns true if value is a letter and false otherwise.

isLetterOrDigit(char value)

Returns true if value is a digit or a letter and false otherwise.

isLowerCase(char value)

Returns true if value is a lowercase letter and false otherwise.

isUpperCase(char value)

Returns true if value is an uppercase letter and false otherwise.

isWhitespace(char value)

Returns true if value is a whitespace character such as space, tab, or newline and false otherwise.

toLowerCase(char value)

Returns a lowercase version of value, with no change if it is not a letter.

toUpperCase(char value)

Returns an uppercase version of value, with no change if it is not a letter.

For example, the variable test contains true after the following code is executed.

boolean test = Character.isLetter('x');

And the variable letter contains 'M' after the following code is executed.

char letter = Character.toUpperCase('m');

These methods can be especially useful when processing input.

Maximum and minimum values

As you recall from Chapter 1, integer arithmetic in Java has limitations. If you increase a large positive number past its maximum value, it becomes a large-magnitude negative number, a phenomenon called overflow. Conversely, if you decrease a large-magnitude negative number past its minimum value, it becomes a large positive number, a phenomenon called underflow.

With floating-point numbers, increasing their magnitudes past their maximum values results in special values that Java reserves to represent either positive or negative infinity, as the case may be. If a floating-point value gets too close to zero, it eventually rounds to zero.

In addition to useful conversion methods, the numerical wrapper classes also have constants for the maximum and minimum values for each type. Instead of trying to remember that the largest positive int value is 2,147,483,647, you can use the equivalent Integer.MAX_VALUE.

The MAX_VALUE constants are always the largest positive number that can be represented with the corresponding type. The MIN_VALUE is more confusing. For integer types, it’s the largest magnitude negative number. For floating-point types, it’s the smallest positive non-zero value that can be represented. Here is a table listing these constants.

Constant Meaning
Byte.MAX_VALUE

Most positive value a byte value can have

Byte.MIN_VALUE

Most negative value a byte value can have

Short.MAX_VALUE

Most positive value a short value can have

Short.MIN_VALUE

Most negative value a short value

Integer.MAX_VALUE

Most positive value an int value can have

Integer.MIN_VALUE

Most negative value an int value can have

Long.MAX_VALUE

Most positive value a long value can have

Long.MIN_VALUE

Most negative value a long value can have

Float.MAX_VALUE

Largest absolute value a float value can have

Float.MIN_VALUE

Smallest absolute value a float value can have

Double.MAX_VALUE

Largest absolute value a double value can have

Double.MIN_VALUE

Smallest absolute value a double value can have

The wrap-around nature of integer arithmetic means that adding 1 to Integer.MAX_VALUE results in Integer.MIN_VALUE. Note that all integer arithmetic in Java is done assuming type int, unless explicitly specified otherwise. Thus, Short.MAX_VALUE + 1 does not overflow to a negative value unless you store the result into a short. The same rules apply to underflow.

Overflow and underflow do not work in the same way with the floating-point numbers represented by float and double. The expression Double.MAX_VALUE + 1 results in Double.MAX_VALUE because 1 is so small in comparison that it’s lost in rounding error. However, 1.5*Double.MAX_VALUE results in Double.POSITIVE_INFINITY, a constant used to represent any value larger than Double.MAX_VALUE. Since Double.MIN_VALUE is the smallest non-zero number, Double.MIN_VALUE - 1 evaluates to -1.0.

Using wrapper classes for storage

Wrapper classes in Java have a split personality. On the one hand, the classes themselves can be used for the utility methods and constants we described above. However, objects of these same wrapper classes can be used in an entirely separate way to store primitive values. Each primitive type can be stored in its wrapper type as shown below.

Integer fingers = new Integer(5);
Double pi = new Double(3.141592);
Character question = new Character('?');

Why would we want to do this? There are many situations in which a library method or data structure requires a reference type, not a primitive type. These wrappers were designed for these cases when you have to treat a primitive type as an object.

Object value = new Integer(42);

To make working with wrapper classes easier, Java 5 and higher support automatic boxing and unboxing, meaning that primitive types will automatically be converted to their wrapper types (and vice versa) when appropriate. Thus, the earlier code could be written as follows.

Integer fingers = 5;
Double pi = 3.141592;
Character question = '?';

Programmers who don’t understand wrapper classes will sometimes use primitive types and wrapper classes interchangeably, mixing double and Double, for example. Avoid using wrapper classes unnecessarily, since they require more memory and more computation to perform operations.

Fortunately, automatic boxing and unboxing reduce the need to think about wrapper classes, and most programmers will rarely need to declare an explicit wrapper reference. We’ll discuss wrapper classes further in Chapter 18, where they are used to allow generic classes to store primitive types as well as reference types.

3.5. Solution: College cost calculator

In this chapter, we introduced and more fully explained many aspects of manipulating data in Java, including declaring variables, assigning values, performing simple arithmetic and more advanced math, inputting and outputting data, and using the type system, which includes subtle differences between primitive and reference types. Our solution to the college cost calculator problem posed at the beginning of the chapter uses all of these features at some level. We present this solution below.

import java.util.*; (1)

public class CollegeCosts {
    public static void main(String[] args) { (2)
        System.out.println("Welcome to the College Cost Calculator!"); (3)
        Scanner in = new Scanner(System.in); (4)
1 The first step in our solution is to import java.util.* so that we can use the Scanner class.
2 After we start the enclosing CollegeCosts class, we begin the main() method.
3 We print a welcome message for the user.
4 Then, we create a Scanner object.

Next is a sequence of prompts to the user interspersed with input done with the Scanner object.

        System.out.print("Enter your first name:\t\t");
        String firstName = in.next(); (1)
        System.out.print("Enter your last name:\t\t");
        String lastName = in.next(); (2)
        System.out.print("Enter tuition per semester:\t$");
        double semesterTuition = in.nextDouble(); (3)
        System.out.print("Enter rent per month:\t\t$");
        double monthlyRent = in.nextDouble(); (4)
        System.out.print("Enter food cost per month:\t$");
        double monthlyFood = in.nextDouble(); (5)
        System.out.print("Annual interest rate:\t\t");
        double annualInterest = in.nextDouble(); (6)
        System.out.print("Years to pay back your loan:\t");
        int years = in.nextInt(); (7)
1 The program reads the user’s first name as a String,
2 the user’s last name as a String,
3 the per-semester tuition cost as a double,
4 the monthly cost of rent as a double,
5 the monthly cost of food as a double,
6 the interest rate for the loan as a double,
7 and the number of years needed to pay back the loan as an int.

The next segment of code completes the computations needed.

        double yearlyCost = semesterTuition * 2.0 + (monthlyRent + monthlyFood) * 12.0; (1)
        double fourYearCost = yearlyCost * 4.0; (2)
        double monthlyInterest = annualInterest / 12.0; (3)
        double monthlyPayment = fourYearCost * monthlyInterest / (4)
            (1.0 - Math.pow(1.0 + monthlyInterest, -years * 12.0));
        double totalLoanCost = monthlyPayment * 12.0 * years; (5)
1 It finds the total yearly cost by doubling the semester cost, multiplying the monthly rent and food costs by 12, and summing the answers together.
2 The four year cost is simply four times the yearly cost.
3 To find the monthly payment, we find the monthly interest by dividing the annual interest rate by 12 and plugging this value into the formula from the beginning of the chapter.
4 Finally, the total cost of the loan is the monthly payment times 12 times the number of years.

All that remains is to print out the output.

        System.out.println("\nCollege costs for " + firstName + " " + lastName ); (1)
        System.out.println("***************************************");
        System.out.format("Yearly cost:\t\t\t$%.2f%n", yearlyCost); (2)
        System.out.format("Four year cost:\t\t\t$%.2f%n", fourYearCost);
        System.out.format("Monthly loan payment:\t\t$%.2f%n", monthlyPayment);
        System.out.format("Total loan cost:\t\t$%.2f%n", totalLoanCost );
    }
}
1 First, we output a header describing the following output as college costs for the user.
2 Using System.out.format() as described in Section 3.3.2, we print out the yearly cost, four year cost, monthly loan payment, and total cost, all formatted with dollar signs, two places after the decimal point, and tabs so that the output lines up.

3.6. Concurrency: Expressions

In Section 2.5, we introduced the ideas of task and domain decomposition that could be used to solve a problem in parallel. By splitting up the jobs to be done (as in task decomposition) or dividing a large amount of data into pieces (as in domain decomposition), we can attack a problem with several workers to finish the work more quickly.

3.6.1. Splitting expressions

Performing arithmetic is some of the only Java syntax we’ve introduced that can be used to solve problems directly, but evaluating a single mathematical expression usually does not warrant concurrency. If the terms in the expression are themselves complex functions (such as numerical integrations or simulations that produce answers), it might be reasonable to evaluate these functions concurrently.

In this section, we give an example of splitting an expression into smaller sub-expressions that could be evaluated concurrently. The basic steps underlying the concurrent evaluation of expressions are the following.

  • Identify sub-expressions that are independent of each other.

  • Create a separate thread to evaluate each sub-expression.

  • Combine the results from each thread to obtain a final answer.

While this sequence of steps looks simple, each step could be complex. Worse, being careless at any step could result in a concurrent solution that runs slower than the sequential solution or even gives the wrong answer. The following example illustrates these steps.

Example 3.22 Split expression

Consider the following statement:

double value = f(a,b)*g(c);

This statement evaluates methods f() and g(), multiplies the computed values, and assigns the result to variable value. In Figure 3.5, we show two ways of evaluating the expression f(a,b)*g(c). Figure 3.5(a) shows sequential evaluation of the expression, where f() is computed, g() follows, and then the two results are multiplied to get the final value. Figure 3.5(b) shows evaluation of the expression in which f() and g() are evaluated concurrently instead.

splitExpressionFigure
Figure 3.5 Computation of value = f(a,b)*g(c) with (a) sequential and (b) concurrent approaches.

On a multicore processor, the computation of f() and g() could be carried out on separate cores. We can create one thread for each method and wait for the threads to complete. Upon completion, we can retrieve the results of each computation and multiply them together as in Figure 3.5(b). Program 3.3 illustrates this concurrent approach.

Program 3.3 Concurrent evaluation of an expression.
public class SplitExpression {
    public static void main(String[] args) {
        ComputeF fThread = new ComputeF(3.14, 2.99); (1)
        ComputeG gThread = new ComputeG(5.55); (2)
        fThread.start(); (3)
        gThread.start(); (4)
        try {
            fThread.join();  (5)
            gThread.join(); (6)
            double fResult = fThread.getResult(); (7)
            double gResult = gThread.getResult(); (8)
            double answer = fResult*gResult; (9)
            System.out.println("Result of f: " + fResult );
            System.out.println("Result of g: " + gResult );
            System.out.println("Final answer: " + answer);        
        }
        catch(InterruptedException e){
            System.out.println("Computation interrupted!");
        }             
    }   
}
1 We create an object named fThread which takes two arguments, 3.14 and 2.99 in this example.
2 We create another object named gThread that takes one, 5.55. Both of these objects have types that extend the Thread class, which means that they can be made to run independently.
3 We start the first thread running.
4 We start the second thread running. Every object whose type is Thread (or a child of Thread, which we will discuss in Chapter 11) has a start() method which begins its execution as a separate thread.
5 We wait for the first thread to finish. How do we know when a thread is done executing? Every Thread object has a join() method. If some code calls a thread’s join() method, the method will not return until the thread is finished. When code is waiting for a thread to finish, it’s possible for it to be interrupted if some other thread has gotten tired of the code waiting around doing nothing. If that happens, an InterruptedException is thrown. Exceptions are the way that Java deals with errors and other unusual situations. We’ll discuss them further in Chapter 12, but for now, you only need to know that code (like the join() method) that can cause certain kinds of exceptions (like the InterruptedException) needs to be enclosed in a try block. After the try block comes a catch block that says what to do in the even of that exception. In our case, we print out "Computation interrupted!"
6 We wait for the second thread to finish.
7 Once the threads have completed their respective tasks, the execution of Program 3.3 resumes, and we obtain the result of the computation done by fThread by calling its getResult() method.
8 On the next line, we call the getResult() method on gThread to obtain its result. Note that we could have called these getResult() methods before the join() calls, but the computations might not have completed, yielding invalid or incorrect results (or crashing the program).
9 Finally, these two computed values are multiplied to get the final result, which is assigned to answer and printed.

We would like to show how classes ComputeF and ComputeG are written, but we’ll hold off since they use concepts relating to methods, class design, and inheritance that we’ll cover in Chapter 8, Chapter 9, and Chapter 11.

If you don’t understand all the elements of Program 3.3, don’t despair! We’re trying to give you an example of what concurrency looks like in Java, but you can’t be expected to master all the details at this stage. However, concurrency in Java will often follow the steps shown:

  1. Creation of Thread (or children of Thread) objects

  2. Calling the start() method on these objects to start them executing

  3. Calling the join() method on them to wait for them to finish

  4. Retrieving the results (if any) of the computations done by the objects

3.6.2. Care in splitting expressions

The above example illustrates how you could split an expression and evaluate it concurrently. Note the following points when deciding whether or not to use concurrency. First, your program will run faster concurrently only if the work done is complex enough that its computation takes significantly longer than the time to create the necessary threads. In the example above, the methods f() and g() must be complex enough that it takes a significant amount of time to evaluate them. Otherwise, concurrency won’t reduce the running time. This aspect of speedup is explained in detail in Chapter 13.

Second, splitting an expression (or any complex sequence of computations) is easy when its individual components are independent. If they are interdependent, splitting requires care to avoid subtle programming errors. Consider the expression f(a) + g(b) and suppose that f() modifies the value of b during execution. Such a modification is called a side effect. This side effect creates a dependency between f() and g(). Concurrent execution of these two methods must be done carefully, if it can be done at all. Chapter 14 discusses concurrency in the presence of dependencies.

3.7. Summary

In a strongly typed language such as Java, types are an important concept. Every literal and variable in Java has a type, which specifies the possible values items with that type could have and the operations that can be done with them. Types are used to catch programming errors at compile time.

Java has a small set of primitive types such as int and double that hold single values and use operators to manipulate them. Java also has reference types, which use primitive types as building blocks, can be created by any Java programmer, can contain arbitrarily complex data, and are manipulated with methods. One of the most commonly used reference types is String, which is used to store text of any length.

A number of library classes have been provided by the developers of Java. Programs performing mathematical operations beyond simple arithmetic may need to use methods from the Math class. Programs that need to generate random numbers can use methods from the Random class. Conversions and other useful manipulations of primitive types are provided by wrapper classes.

We also gave a taste of the syntax for creating, running, and waiting for the completion of threads. Such threads could be used to speed up the evaluation of computations on multicore processors, but only if the computations are long, complex, and not too interdependent.

3.8. Exercises

Conceptual Problems

  1. What is the difference between the set of integers from mathematics and the sets defined by int and long?

  2. In Example 3.7, the sum of two int variables was another int value, which could not be stored into a byte variable. Would this code have worked if variables a and b had been declared with type byte? What if a was assigned 121 and b was assigned 98?

  3. The following three statements are legal Java (if properly included inside of a method). However, if we changed 2 to 2.0 or 5 to 5.0, the statements would not be legal. Explain why.

    float roomArea = 2;
    float homeArea = 5;
    float area = roomArea * homeArea;
  4. Consider the following variable declarations.

    int x = 3, y = 4, z = -9;
    float p = 3.99f, q = -9.89f;
    int population1 = 15000, population2 = 8000;
    final double MAXIMUM_LEVEL = 350;
    double limitPerCapita = 0.03;
    int age = 14;
    final int MAXIMUM_AGE = 23;
    boolean allowed = false;

    Now evaluate each of the following expressions to true or false.

    1. MAXIMUM_LEVEL/population1 > limitPerCapita && MAXIMUM_LEVEL/population2 < limitPerCapita

    2. MAXIMUM_LEVEL/population1 > limitPerCapita || MAXIMUM_LEVEL/population2 < limitPerCapita

    3. age < MAXIMUM_AGE && allowed

    4. (x < y && y > z) || (p > q && population1 < population2)

  5. Evaluate the following expressions by hand and then check the results with a Java compiler.

    1. 5 & 6

    2. 5 | 6

    3. 5 ^ 6

    4. ~5

    5. 5 >> 2

    6. 5 << 2

    7. 5 >>> 2

  6. Evaluate the following expressions by hand and then check the results with a Java compiler.

    1. Byte.MIN_VALUE - 1

    2. Byte.MAX_VALUE + 1

    3. Integer.MIN_VALUE - 1

  7. Evaluate the following expressions by hand and then check the results with a Java compiler.

    1. Float.MAX_VALUE + 1

    2. Double.MAX_VALUE - 1

    3. -Double.MAX_VALUE - 1

    4. -Double.MIN_VALUE - 1

    5. -Double.MIN_VALUE + 1

  8. When evaluated in Java, the expression 2*Double.MAX_VALUE results in Double.POSITIVE_INFINITY to indicate that the maximum representable value has been exceeded. However, the expression Double.MAX_VALUE + 1 results in Double.MAX_VALUE. Why doesn’t the second case yield Double.POSITIVE_INFINITY as well?

  9. Explain what is printed when the following statements are executed.

    System.out.println(15 + 20);
    System.out.println("15" + 20);
    System.out.println("" + 15 + 20);
  10. For each of the following Java expressions, indicate the types of each value being used and the type of the result when the expression is evaluated.

    1. 3 + 4

    2. 3 + 4.0

    3. 3.0 + 4.0

    4. 3.0f + 4.0

    5. (double)(3 + 4)

    6. (double)(3.0 + 4.0)

    7. Math.round(3 * 4.2)

    8. Math.round(3.2 * 4.9)

    9. Math.round(15.5 * 4.0)

    10. (int)(15.5 * 4.0)

    11. Math.round(3.154)

  11. For each of the following expressions, determine the maximum amount of concurrency that can be achieved. Using a diagram similar to Figure 3.5(b), show how the computation of each expression will proceed. Assume there are no side effects. Note that you can create separate threads for multiple instances of method f().

    1. f(a) + f(b) + f(c)

    2. f(a * g(b))

    3. f(g(a)) + f(b) + f(c)

  12. Answer the following questions about types, values, and references.

    1. What is the difference between a value and the type that the value has?

    2. In Java, the primitive type int represents a limited set of integers, not the entire set of integers from mathematics. Why is this the case? Why didn’t the designers of Java allow int to represent all integers?

    3. How are operations defined for reference types?

    4. Explain the subtle difference between a reference and an object in Java.

  13. Consider the following declarations of three Car objects.

     Car car1 = new Car("Mercedes", "C300 Sport", 75000);
     Car car2 = new Car("Pontiac", "Vibe", 17000);
     Car car3 = new Car("Mercedes", "C300 Sport", 75000);

    Let same be a variable of type boolean. What is the value of variable same after each of the following statements? Assume that the equals() method will return true if all of the attributes specified by the constructors for the two objects are the same.

    1. same = (car1 == car2);

    2. same = (car1 == car3);

    3. same = car1.equals(car3);

    4. car2 = car3;

    5. same = (car2 == car1);

    6. same = (car2 == car3);

    7. same = car2.equals(car3);

  14. Characters 'a' and 'A' have Unicode values \u0061 and \u0041, respectively. Give the representation of these two characters as 16-bit unsigned binary integers.

  15. Assuming that each character occupies 16 bits (two bytes) in memory and is encoded using Unicode, use hexadecimal numbers to show how the word "Java" will be represented in computer memory. Unicode values for the Latin alphabet are the same as the values for the older ASCII standard. You can find a listing of these values on many websites such as AsciiTable.

  16. What is the output from the following sequence of statements?

    String p = "Break it";
    String q = "down like this!";
    System.out.println((p + q).length());
  17. What is the output from the following sequence of statements? Note that r contains a single space character.

    String p = "This is not a string.";
    String q = "";
    String r = " ";
    System.out.println((p + q + r).length());

Programming Practice

  1. Try compiling the following program and observe the error reported by the compiler.

    public class UninitializedString {
         public static void main(String[] args) {
              String greeting;
              System.out.println(greeting);
        }
    }

    Now initialize the greeting object and rerun the program. Why does the program compile now?

  2. Write a Java program that prompts the user to enter the number of rooms in her home, uses a Scanner object to read the input into an int variable named rooms, and then outputs the value on the screen. If you compile and execute your program and type in the value 3.5, can you explain the output you see?

  3. Convert the college cost calculator solution given in Section 3.5 to use JOptionPane methods for both input and output. Use the header giving the user’s first and last name for the title of the output dialog and omit the line of asterisks. If you put all the output in a single String with a newline (\n) separating each line, the output will display properly.

4. Selection

Life is a sum of all your choices.

— Albert Camus

4.1. Problem: Monty Hall simulation

There’s a famous mathematical puzzle called the Monty Hall problem, based on the television show Let’s Make a Deal hosted by the eponymous Monty Hall. In this problem, you’re presented with three doors. Two of the three doors have junk behind them. One randomly selected door conceals something like a pile of gold. If you can choose that door, you win the gold. After you make an initial choice, Monty, who knows which door the pile of gold is behind, will open one of the two other doors, always picking a door with junk behind it. If you chose the gold door, Monty will pick between the two junk doors randomly. After opening a door, Monty gives you a chance to switch to the other unopened door. You decide to switch or not, the appropriate door is opened, and you win either junk or a pile of gold, depending on your luck.

As it turns out, it’s always a better strategy to switch doors. If you keep your initial choice, you have a 1 in 3 chance to win the gold. However, if you switch doors, you’ll have a 2 in 3 chance. The problem is counterintuitive and leads many people, including mathematicians and people holding advanced degrees, to the incorrect answer.

Think about it this way: Suppose you could pick two doors to open, and if the gold was behind either one of them, you’d win. Clearly, you’d have a 2 in 3 chance of winning. Monty allows you this option. Just pick the two doors you want and tell Monty the third. He reveals one of your two initial doors as junk, and you switch to the other one.

If you still aren’t convinced, that’s fine. Your goal is to write a program that simulates the Monty Hall dilemma, allowing a user to guess a door and then potentially switch. Once you’ve written the simulation, you can choose to play repeatedly and see how well you do if you switch.

A Monty Hall scenario has two significant features that distinguish it from problems in previous chapters. First, randomness play a role. Generating random numbers has become an important part of computer science, and most languages provide programmers with tools for generating random or practically random numbers. Recall the Random class from Chapter 3. With an object of type Random called random, you can generate a random int between 0 and n - 1 by calling random.nextInt(n).

The second and much more important feature of Java in the solution to this problem is the element of choice. A random door is chosen to hide gold, and the program must react appropriately. The user chooses a door, and the program must carefully choose another door to open in response. Finally, the user must decide whether or not he or she wants to switch his or her choice. Inherent in this problem is the idea of conditional execution. Every program from the previous chapter runs sequentially, line by line. With conditional execution, only some of the code may be executed, depending on input from the user or the values that random numbers take. Previously, every program was deterministic, a series of inevitable consequences. Now, that linear, one-thing-follows-another paradigm has split into complex trees and webs of possible program executions.

4.2. Concepts: Choosing between options

Before we get to random numbers and the complex choices involved in the Monty Hall problem, let’s talk about the simplified approach that most programming languages take to conditional execution. When we come to a point in a program where there is a choice to be made, we can think of it as the question, “Do I want to perform this series of tasks?” Like the classic game of 20 Questions, these questions in Java generally only have two answers: “yes” or “no.” If the answer is “yes,” the program completes Task A, otherwise it completes Task B. It’s easier to design programming languages that can handle yes-or-no questions than any general question. If you’ve studied logic in the past, you have probably run across Boolean logic. Boolean logic gives a set of rules, similar to the rules of traditional algebra, that can be applied to a system with only two values: true and false.

4.2.1. Simple choices

Because we want to build a system using only yes-or-no questions, Boolean logic is a perfect fit for computer science. To conform with other computer scientists, we try to think of conditions in terms of true and false, instead of yes and no. Thus, we can begin to formulate the kinds of choices we want to make:

If it’s raining outside,

I’ll take my umbrella.

This statement is a very simple program, even though it’s not one executed by a computer. The person following this program asks herself, “Is it raining today?” If the answer is “yes,” then she’ll take her umbrella. We can abstract this idea a bit further by saying that raining outside is a condition p and that taking my umbrella is an action a. In other words, if p is true, then do a. We haven’t specified what is to be done if p is not true, although we can assume that the actor in this drama will not take an umbrella.

If we want to view p as a decision to make, we can specify what happens if it’s not true. For example, we could formulate another choice:

If I have at least $50 in my pocket,

I’ll eat a lobster dinner;

otherwise,

I’ll eat fast food.

In this case, we let having at least $50 be condition q, eating a lobster dinner be action b, and eating fast food be action c. Now we’ve created a decision. If q is true, the person will do action b, but if it’s false, she’ll do action c.

4.2.2. Boolean operations

Even by itself, the ability to pick between two options is powerful, but we can augment this ability in a couple of ways. First, we don’t have to rely on simple conditions. Using Boolean logic, we can make arbitrarily complex conditions.

If I’m bored, or it’s late and I can’t sleep,

I’ll watch television.

Someone following this program will watch television if he’s bored or if it’s late and he also can’t sleep. We can break the condition into three sub-conditions: I’m bored is condition x, it’s late is condition y, and I can’t sleep is condition z. We have connected these three conditions together using the words “and” and “or.” These two simple words represent powerful concepts in Boolean logic, AND and OR. When two conditions are combined with AND, the result is true only if both conditions are true. When two conditions are combined with OR, the result is true if either of the conditions is true.

We can create a table called a truth table to show all the possible values certain conditions can take. We’re going to use the symbol ∧ to represent the concept of AND and the symbol ∨ to represent the concept of OR. We’ll also abbreviate true to T and false to F.

Given a condition x, a condition y, and the condition made by x ∧ y, this truth table shows all possible values. As stipulated, x ∧ y is true only when both x and y are true.

x y x ∧ y

T

T

T

T

F

F

F

T

F

F

F

F

This truth table gives all the values for x ∨ y. As you can see, x ∨ y is true if x or y are true.

x y x ∨ y

T

T

T

T

F

T

F

T

T

F

F

F

There’s confusion surrounding the word “or” in English. Sometimes “or” is used in an exclusive sense to mean one or the other but not both, as in, “Would you like lemonade or iced tea with your meal?” In logic, this exclusive or exists as well and is called XOR. This difference gives another reason for a formally structured language like mathematics or Java to express ourselves precisely. When two conditions are connected with XOR, the result is true if one or the other but not both conditions are true. We use the symbol ⊕ to represent the XOR operation in the truth table below.

This truth table gives all the values for x ⊕ y.

x y x ⊕ y

T

T

F

T

F

T

F

T

T

F

F

F

The operations AND, OR, and XOR are all binary operations like addition and multiplication. They connect two conditions together to get a result. There’s also a single unary operation in Boolean logic, the NOT operator. A NOT simply reverses a condition. If a condition is true, then NOT applied to that condition will yield false, and vice versa.

Here’s a truth table for NOT, using the symbol ¬ to represent the NOT operation.

x ¬x

T

F

F

T

Now that we’ve nailed down some notation for Boolean logic, we can express the complicated expression that sent us down this path in the first place. Recall that x is I’m bored, y is it’s late, and z is I can’t sleep. Let d be the action I’ll watch television. We can express the choice in this way: If x ∨ (y ∧ z), then do d. Using this notation, we’ve expressed precisely the conditions for watching television, using parentheses to clear up the ambiguity present in the original statement. If we can map individual conditions to Boolean variables, we can build conditions of arbitrary complexity.

4.2.3. Nested choices

Making one choice is all well and good, but in life and computer programs, we may have to make many interrelated choices. For example, if you choose to eat at a seafood restaurant, then you might choose between eating shrimp and lobster, but if you choose instead to eat at a steakhouse, the options of shrimp and lobster might not be available.

A nested choice is one that sits inside of another choice you’ve already made. We could describe choices of restaurants and meals as follows.

If I want seafood,

I’ll eat at Sharky’s, where

if I have at least $50,

I’ll order the lobster;

otherwise,

I’ll order the shrimp.

But if I don’t want seafood,

I’ll eat at the Golden Calf, where

if I have at least $30,

I’ll order the filet mignon;

otherwise,

I’ll order the pork chops.

The previous description is long, but it precisely expresses the decisions our imaginary diner might make. This description in English has drawbacks: It’s long and repetitive, and the grouping of specific meal choices with specific restaurants isn’t clear.

In the next section, we discuss Java syntax that allows us to express the same sorts of decision patterns. Unlike English, Java has been designed to make these sequences of decisions clear and easy to read.

4.3. Syntax: Selection in Java

With some theoretical background on the kinds of choices we’re interested in making, we can now discuss the Java syntax used to describe these choices. It was no accident that we kept repeating the word “if,” because the main Java language feature for making choices is called an if statement.

4.3.1. if statements

The designers of Java studied Boolean logic and created a type called boolean. Every condition used by an if statement must evaluate to a boolean value, which can only be one of two things: true or false.

For example, we could have a boolean variable called raining. Stored in this variable is the value true if it’s raining and false if it isn’t. Using Java syntax, we could encode our first example in which our actor takes her umbrella when it’s raining.

if(raining) {
    umbrella.take();
}

The action taken if it is raining is done by calling a method on an object. We’ll discuss objects and methods further in Chapter 8 and Chapter 9. What we’re focusing on now is that the line umbrella.take(); is executed only if raining has the value true. Nothing is done if it’s false. Figure 4.1 shows this pattern of conditional execution followed by all if statements.

if
Figure 4.1 Execution goes inside the if statement when its condition is true and skips past it otherwise.

Our descriptions of logical scenarios from the previous section used the word “then” to mark the actions that would be done if a condition was true. Some languages use then as a keyword, but Java doesn’t. Instead, note the left brace ({) and the right brace (}) that enclose the executable line umbrella.take();. These braces serve the same role as the word “then,” clearly marking the action to be performed if a condition is true. Braces are unambiguous because they mark a start and an end. If there are many actions to be done, they can all be put inside the braces, and there will be no question as to which actions are associated with a given if statement.

For example, we may also need to close the window and put on a raincoat if it’s raining. We might accomplish these tasks in Java as follows.

if(raining) {
    umbrella.take();
    window.close();
    raincoat.putOn();
}

Within a matching pair of braces ({ }), called a block of code, execution proceeds normally, line by line. First, the JVM will cause the umbrella to be taken, then the window to be closed, and finally the raincoat to be put on.

If only a single line of code is contained within a block of code, the braces can be left out. For example, many experienced Java programmers would have written our first example as follows.

if(raining)
    umbrella.take();

For beginning Java programmers, however, it’s a good idea to use braces even when you don’t need to. Without braces, code can appear to be doing one thing when it’s really doing another.

Since programmers must often choose between two alternatives, Java provides an else statement to specify code that should be run when the condition of the if statement is false.

Let fiftyDollars be a boolean variable that’s true if we have at least $50 and is false otherwise. Now, we can choose between two dining options based on how much money we have.

if(fiftyDollars) {
    lobsterDinner.eat();
}
else {
    fastFood.eat();
}

This Java code matches the logical statements we wrote before. If we have enough money, we’ll eat a lobster dinner; otherwise, we’ll eat fast food. As with an if statement, we use braces to mark a block of code for an else statement, too. Since a single line of code will be executed in each case, the braces are optional here. We could have written code with the same functionality as follows.

if(fiftyDollars)
    lobsterDinner.eat();
else
    fastFood.eat();

Figure 4.2 shows the pattern of conditional execution followed by all if statements that have a matching else statement.

else
Figure 4.2 Execution goes inside the if statement when its condition is true and jumps into the else statement otherwise.
Pitfall: Misleading indentation

Indentation is used to make code more readable, but Java ignores whitespace, meaning that the indentation has no effect on the execution of the code. To demonstrate, let’s assume that our imaginary diner knows he’ll get a stomachache after eating fast food. Thus, he’ll take some Pepto-Bismol after eating it. If you added this action to the code above, which does not contain braces, you might get the following.

if(fiftyDollars)
    lobsterDinner.eat();
else
    fastFood.eat();
    peptoBismol.take();

Although it looks like both fastFood.eat(); and peptoBismol.take(); are within the block of the else statement, only fastFood.eat(); is. The line peptoBismol.take(); is not part of the if-else structure at all and will be executed no matter what. The correct way to program this decision is below.

if(fiftyDollars)
    lobsterDinner.eat();
else {
    fastFood.eat();
    peptoBismol.take();
}

4.3.2. The boolean type and its operations

Recall that Java uses the type boolean for values that can only be true or false. Just like the numerical types double and int, the boolean type has specific operations that can be used to combine them together. By design, these operations correspond exactly to the logical operations we described before. Here’s a table giving the Java operators equivalent to the logical Boolean operations.

Name Math
Symbol
Java
Operator
Description

AND

∧

&&

Returns true if both values are true

OR

∨

||

Returns true if either value is true

XOR

⊕

^

Returns true if values are different

NOT

¬

!

Returns the opposite of the value

Using these operators, we can create boolean values and combine them together.

boolean x = true;
boolean y = false;
boolean z = !((x || y) ^ (x && y));

When this code is executed, the value of z will be false. Although it’s perfectly legal to perform boolean operations this way, it’s much more common to combine them “on the fly” inside of the condition of an if statement. Recall the statement from the previous section:

If I’m bored, or it’s late and I can’t sleep,

I’ll watch television.

If we let bored, late, and canSleep be boolean variables whose values indicate if we are bored, if it is late, and if we can sleep, respectively, we can encode this statement in Java like so.

if(bored || (late && !canSleep))
    television.watch();

Combining the || operator with other || operators is both commutative and associative: order and grouping doesn’t matter. Likewise, combining the && operator with other && operators is also commutative and associative. However, once you start mixing || with &&, it’s a good idea to use parentheses for grouping. If, in the above example, bored is true, late is false, and canSleep is true, then the expression bored || (late && !canSleep) will be true. However, with the same values, the expression bored || late && !canSleep will be false.

Now that we’re discussing ordering, note that || and && are short circuit operators. Short circuit means that, if the value of the expression can be determined without evaluating the rest of it, the JVM won’t bother to compute any more of the expression. With || this situation arises because true OR anything else is still true. With && this situations arises because false AND anything else is still false.

if(true || ((late && !canSleep && isTired && isHungry) ||
    (wantsToFindOutWhatHappensNextInHisFavoriteShow ||
    likesTV)))

The condition of this if statement will always evaluate to true, and its body will always be executed. Because Java knows this, it won’t even bother to check any of the conditions after the first || operator. This short circuit evaluation is done at run time and will work if the value of a variable at the beginning of an OR clause is true. It need not be the literal true.

if(false && ((late || !canSleep || isTired || isHungry) &&
    (wantsToFindOutWhatHappensNextInHisFavoriteShow ||
    likesTV)))

The condition of this if statement will always evaluate to false and its body won’t be executed. As before, nothing after the first && will even be checked. If you’re combining literals and boolean values with the || and && operators, it makes no difference that short circuit evaluation occurs. However, if a method call is part of the clauses, your code might miss valuable side-effects. For example, let the boolean variable working be false in the following.

if(working && doSomethingImportant())

In this case, the doSomethingImportant() method must return a boolean value to be a valid statement. Still, if working is false, the doSomethingImportant() method won’t even be called. As soon as the JVM realizes that it’s applying the && operation to a false value (or an || to a true), it’ll give up. In many cases, doing so is fine. In fact, programmers sometimes exploit this feature to allow code in a method like doSomethingImportant() to run only if it’s safe to do so. In this case, if we assume that we always want to run the doSomethingImportant() method (because it does something important) every time the condition of the if statement is evaluated, we need to restructure the code. For example, we can reverse the order of the two terms in the AND clause to achieve this effect. Alternatively, Java provides non-short circuit versions of the || and && operators, namely | and &, if you need to force full evaluation.

You might have been wondering where the majority of boolean values come from. Most computer programs don’t ask the user a long series of true or false questions before spitting out an answer. Most boolean values in Java programs are the result of comparisons, often of numerical data types.

It’s can be useful to compare two numbers to see if one is larger, smaller, or equal to the other. For example, you might have a double variable called pressure that gives the water pressure in a hydraulic system. Perhaps you also have a constant called CRITICAL_PRESSURE that gives the maximum safe pressure for your system. You can compare these values using the > operator.

if(pressure > CRITICAL_PRESSURE)
    emergencyShutdown();

This code allows you to call the appropriate emergency method when pressure is too high. Of course, the > operator is not the only way to compare two values in Java. We list all the relational operators in Chapter 3, but the table below shows them again in a mathematical context.

Table 4.1 Relational operators
Name Math
Symbol
Java
Operator
Description

Equals

=

==

true if the two values are equal

Not Equals

≠

!=

true if the two values are not equal

Less Than

<

<

true if the first value is strictly less than the second

Less Than or Equals

≤

<=

true if the first value is less than or equal to the second

Greater Than

>

>

true if the first value is strictly greater than the second

Greater Than or Equals

≥

>=

true if the first value is greater than or equal to the second

The concepts and mathematical symbols for these operators should be familiar from mathematics. There are a few differences from the mathematical versions of these ideas that are worth pointing out. First, only easy-to-type symbols are used for Java operators. Thus, we need two characters to represent most relational operators in the language. These operators can be used to compare any numerical type with any other numerical type, including char. In the case of mismatched types, such as an int and a double, the lower precision type is automatically cast to the higher precision type. Care should be taken when using the == operator with floating-point types because of rounding errors. For example, the expression 1.0/3.0 == 0.3333333333 always evaluates to false.

The == operator is not the same as the = operator from previous chapters. In Java, the double equal sign == is used to compare two things while the single equal sign = is used to assign one thing to another.

Confusion can also arise because, in the mathematical world, relational symbols are used to make a statement: x < y is an announcement or a discovery that the value contained in x is, in fact, smaller than the value contained in y. In the Java world, the statement x < y is a test whose answer is true if the value contained in x is smaller than the value contained in y and false otherwise. Using these operators means performing a test at a specific point in the code, asking a question about the values that certain variables or literals (or the results of method calls) have at that moment in time. In another sense, using these comparisons is a way to take numerical data and convert it into the language of boolean values. Note that the following statement does not compile in Java.

if(4)
    x = y + z;

To be used in an if statement, the value 4 must be first compared with some other numerical type to yield a true or false.

Pitfall: Assignment instead of equality

A common pitfall is to forget one of the equal signs in the comparison operator.

if(x = 4)
    x = y + z;

Again, this code won’t compile. If it did, the variable x would be assigned the value 4, which would in turn be given to the if statement, but an if statement doesn’t know what to do with anything other than a boolean value.

Extreme care should be taken when comparing two boolean values. For example, we might have two boolean values likesDogs1 and likesDogs2, corresponding to whether or not two different people like dogs. Let’s say that the value of each one is true if the person likes dogs and false otherwise. We could create an if statement that would work only if their values are the same.

if(likesDogs1 == likesDogs2)
    makeRoommates();

This code correctly calls the makeRoommates() method only if the two individuals feel the same way about dogs. It doesn’t matter if they both like or dislike dogs, but one roommate loving dogs and the other hating dogs will lead to trouble. However, a tiny mistake in the code could yield the following.

if(likesDogs1 = likesDogs2)
    makeRoommates();

In this case, likesDogs1 would be assigned to whatever likesDogs2 is. Then, that value would be given to the if statement. Here, the makeRoommates() method will be called only if likesDogs2 is true, meaning that the second person likes dogs. Thus, the two people will become roommates if the second one likes dogs, and the feelings of the first person won’t be considered. Unlike the x = 4 example, this code will compile with no warning.

The next few examples illustrate the use of the if statement. They also use some methods from class Math.

Example 4.1 Leap year

In the standard Gregorian calendar, leap years occur roughly once every four years. During leap years, the month of February has 29 days instead of 28. This extra day makes up for the fact that it takes a little more than 365.24 days for the earth to orbit the sun. Unfortunately, the orbit of the earth around the sun doesn’t match up in any exact way with the rotation of the earth, making exceptions to the rule of every four years.

In fact, the official definition for a leap year is a year that is evenly divisible by 4, except for those years that are evenly divisible by 100, with the exception to the exception of years that are evenly divisible by 400. For example, 1988 was a leap year because it was divisible by 4. The year 1900 was not a leap year because it was divisible by 100 but not by 400, and the year 2000 was a leap year because it was divisible by 400.

Recall that the mod operator (%) allows us to find the remainder after integer division. Thus, if n % 100 gives zero, n has no remainder after being divided by 100 and must be evenly divisible by 100.

Program 4.1 Prompts the user for a year and then determines whether or not it is a leap year.
import java.util.*;

public class LeapYear {
    public static void main(String[] args) {                
        Scanner in = new Scanner( System.in );      
        System.out.print("Please enter a year: ");
        int year = in.nextInt();
        if( year % 400 == 0 )
            System.out.println(year + " is a leap year.");
        else if( year % 100 == 0 )
            System.out.println(year + " is not a leap year.");
        else if( year % 4 == 0 )
            System.out.println(year + " is a leap year.");
        else
            System.out.println(year + " is not a leap year.");
    }
}

As with all of the programs in this section, we begin by importing java.util.*, which is needed for the Scanner class for input. The program prompts the user for a year and reads it in. If the year is evenly divisible by 400, the program outputs that it’s a leap year. Otherwise, if the year is evenly divisible by 100, the program outputs that it is not a leap year. Otherwise, if the year is evenly divisible by 4, the program outputs that it’s a leap year. Finally, if all the other conditions have failed, the program outputs that the year is not a leap year.

Example 4.2 Quadratic formula

The quadratic formula is a useful tool from mathematics. Using this formula, you can solve equations of the form ax2 + bx + c = 0. You might recall the statement of the quadratic formula given below.

quadratic

The b2 - 4ac part of the formula is called the discriminant. If the discriminant is positive, there will be two real answers to the equation. If the discriminant is negative, there will be two complex answers to the equation. Finally, if the discriminant is zero, there will be a single real answer to the problem. If you want to write a program to solve quadratic equations for you, it should take these three possibilities into account.

Program 4.2 Solves a quadratic equation.
import java.util.*;

public class Quadratic {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);
        System.out.println("This program solves quadratic" + 
            " equations of the form ax^2 + bx + c = 0.");
        System.out.print("Please enter a value for a: "); (1)
        double a = in.nextDouble();
        System.out.print("Please enter a value for b: ");
        double b = in.nextDouble();
        System.out.print("Please enter a value for c: ");
        double c = in.nextDouble();
        double discriminant = b*b - 4*a*c; (2)
        if(discriminant == 0.0) (3)
            System.out.println("The answer is x = " + (-b/(2*a)));
        else if(discriminant < 0.0) (4)
            System.out.println("The answers are x = " + (-b / (2*a)) + " + "
				+ Math.sqrt(-discriminant) / (2*a) + "i and x = "
				+ (-b / (2*a)) + " - " + Math.sqrt(-discriminant) / (2*a) + "i");
        else (5)
            System.out.println("The answers are x = " + (-b + Math.sqrt(discriminant))/(2*a)
				+ " and x = " + (-b - Math.sqrt(discriminant))/(2*a));
    }
}
1 This program prompts the user and reads in values for a, b, and c.
2 Then, it computes the discriminant.
3 In the first case, we test to see if the discriminant is zero and print the single answer.
4 Next, if the discriminant is negative, we compute the real and complex parts separately and output the two answers.
5 Finally, if the discriminant is positive, we find the two real answers and output them.

Note that braces were not needed for the if, else-if, and else blocks because each is composed of only a single line of code. Although these System.out.println() method calls may take up more than one line visually, Java interprets them as single lines because they each only have a single semicolon (;).

The line if(discriminant == 0.0) is dangerous since we’re using double values. Because of rounding errors, the discriminant might not be exactly zero even if it should be, mathematically. Industrial strength code would probably check to see if the absolute value of the discriminant is less than a very small number (such as 0.00000001). Values that small would then be treated as if they were zero.

Example 4.3 20 Questions

In the time-honored game of 20 Questions, one person mentally chooses something, and the other participants must guess what the thing is by asking questions whose answer is either “yes” or “no.” In one popular version, the person who chooses the thing starts by declaring whether it is animal, vegetable, or mineral.

Using counting principles from math, 20 yes-or-no questions makes it possible to differentiate 220 = 1,048,576 items. If you’re also told whether the thing is animal, vegetable, or mineral, it should be possible to guess over 3 million items! At this point in our development as Java programmers, we’re not yet ready to deal with such a large range of possibilities. To keep the size of the code reasonable, let’s narrow the field to 10 different items: a lizard, an eagle, a dolphin, a human, some lead, a diamond, a tomato, a peach, a maple tree, and a potato.

flowchart
Figure 4.3 Decision tree to distinguish 10 items.

Using these items, we can construct a tree of decisions, starting with the decision between animal, vegetable, and mineral. If the thing is an animal, we could then ask if it is a mammal. If it is a mammal, we could ask if it lives on land, deciding between human and dolphin. If it’s not a mammal, we could ask if it flies, deciding between an eagle and a lizard. We can construct similar questions for the things in the vegetable and mineral categories, matching Figure 4.3.

Program 4.3 Navigates the possible choices in the decision tree.
import java.util.*;

public class TwentyQuestions {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Is it an animal, vegetable, or mineral? (a, v, or m): ");
        String response = in.next().toLowerCase();
        if(response.equals("a")) {
            System.out.print("Is it a mammal? (y or n): ");
            response = in.next().toLowerCase();
            if(response.equals("y")) {
                System.out.print(
                    "Does it live on land? (y or n): ");
                response = in.next().toLowerCase();
                if(response.equals("y"))
                    System.out.println("It's a human.");                
                else // Assume "n"
                    System.out.println("It's a dolphin.");              
            }
            else { // Assume "n"
                System.out.print("Does it fly? (y or n): ");
                response = in.next().toLowerCase();
                if(response.equals("y"))
                    System.out.println("It's an eagle.");               
                else // Assume "n"
                    System.out.println("It's a lizard.");
            }
        }
        else if(response.equals("v")) {
            System.out.print("Is it a fruit? (y or n): ");
            response = in.next().toLowerCase();
            if(response.equals("y")) {
                System.out.print(
                    "Does it grown on a vine? (y or n): ");
                response = in.next().toLowerCase();
                if(response.equals("y"))
                    System.out.println("It's a tomato.");               
                else // Assume "n"
                    System.out.println("It's a peach.");                
            }
            else { // Assume "n"
                System.out.print("Is it a tree? (y or n): ");
                response = in.next().toLowerCase();
                if(response.equals("y"))
                    System.out.println("It's a maple tree.");
                else // Assume "n"
                    System.out.println("It's a potato.");
            }
        }
        else { // Assume "m"
                System.out.print(
                    "Is it the hardest mineral? (y or n): ");
                response = in.next().toLowerCase();
                if(response.equals("y"))
                    System.out.println("It's a diamond.");              
                else // Assume "n"
                    System.out.println("It's lead.");
        }       
    }
}

The code in this example is straightforward, although even 10 items makes for a lot of if and else blocks. Other than the if-else statements, only simple input and output are needed to make the program function. For proper String comparison, it’s necessary to use the equals() method to test if two String values are the same.

Note that we’ve added comments specifying what we assume is the case for each else block. If we were being more careful, we should test for the "y" and "n" cases and then give an error message when the user inputs something unexpected, like "x" or "149" or even "no". Again, note that no braces are needed for the final if-else blocks in which the guess is made, since each of these guesses requires only a single line of code.

You might be curious how to make a real 20 Questions game that could learn over time. To do so, many more programming tools are necessary: repetition, data structures (so that you can organize the questions), and file input and output (so that you can store new information permanently). These concepts are covered in later chapters.

4.3.3. switch statements

The if statement is the workhorse of Java conditional execution. With enough care, you can craft code that can make any fixed sequence of decisions with arbitrary complexity. Even so, the if statement can be a little clumsy because it only allows you to choose between two alternatives. After all, a conditional can only be true or false. Certainly, decisions can be nested, allowing for more than two possibilities, but long lists of possibilities can be cumbersome and hard to read.

For example, imagine that we want to create a program that determines the appropriate gift for a wedding anniversary. Below is a table of traditional categories of gifts based on the anniversary year.

Year Gift Year Gift

1

Paper

13

Lace

2

Cotton

14

Ivory

3

Leather

15

Crystal

4

Fruit

20

China

5

Wood

25

Silver

6

Candy / Iron

30

Pearl

7

Wool / Copper

35

Coral

8

Bronze / Pottery

40

Ruby

9

Pottery / Willow

45

Sapphire

10

Tin / Aluminum

50

Gold

11

Steel

55

Emerald

12

Silk / Linen

60

Diamond

Let year be a variable of type int containing the year in question. A structure of if-else statements that can determine the appropriate gift based on the year is below.

String gift;
if(year == 1)
    gift = "Paper";
else if(year == 2)
    gift = "Cotton";
else if(year == 3)
    gift = "Leather";
else if(year == 4)
    gift = "Fruit";
else if(year == 5)
    gift = "Wood";
else if(year == 6)
    gift = "Candy / Iron";
else if(year == 7)
    gift = "Wool / Copper";
else if(year == 8)
    gift = "Bronze / Pottery";
else if(year == 9)
    gift = "Pottery / Willow";
else if(year == 10)
    gift = "Tin / Aluminum";
else if(year == 11)
    gift = "Steel";
else if(year == 12)
    gift = "Silk / Linen";
else if(year == 13)
    gift = "Lace";
else if(year == 14)
    gift = "Ivory";
else if(year == 15)
    gift = "Crystal";
else if(year == 20)
    gift = "China";
else if(year == 25)
    gift = "Silver";
else if(year == 30)
    gift = "Pearl";
else if(year == 35)
    gift = "Coral";
else if(year == 40)
    gift = "Ruby";
else if(year == 45)
    gift = "Sapphire";
else if(year == 50)
    gift = "Gold";
else if(year == 55)
    gift = "Emerald";
else if(year == 60)
    gift = "Diamond";
else
    gift = "No traditional gift";

This code stores the correct value in gift. Note that we are using the feature of if statements that treats an entire if statement as one statement. If we used braces to group things properly, the code would become unreadable and unmanageably large.

String gift;
if(year == 1) {
    gift = "Paper";
}
else {
    if(year == 2) {
        gift = "Cotton";
    }
    else {
        if(year == 3) {
            gift = "Leather";
        }
        else {
            if(year == 4) {
                gift = "Fruit";
            }
            .
            .
            .

It appears that there’s some kind of else if construct in Java, but there isn’t. Still, careful use of the rules for braces allows us to write code that nicely expresses a sequence of alternatives.

Another way of expressing a long sequence of alternatives is by using a switch statement. A switch statement takes a single integer type value (int, long, short, byte, char) or a String and jumps to a case corresponding to the input. We can recode the anniversary gift example using a switch statement as follows.

String gift;
switch(year) {
    case 1:  gift = "Paper"; break;
    case 2:  gift = "Cotton"; break;
    case 3:  gift = "Leather"; break;
    case 4:  gift = "Fruit"; break;
    case 5:  gift = "Wood"; break;
    case 6:  gift = "Candy / Iron"; break;
    case 7:  gift = "Wool / Copper"; break;
    case 8:  gift = "Bronze / Pottery"; break;
    case 9:  gift = "Pottery / Willow"; break;
    case 10: gift = "Tin / Aluminum"; break;
    case 11: gift = "Steel"; break;
    case 12: gift = "Silk / Linen"; break;
    case 13: gift = "Lace"; break;
    case 14: gift = "Ivory"; break;
    case 15: gift = "Crystal"; break;
    case 20: gift = "China"; break;
    case 25: gift = "Silver"; break;
    case 30: gift = "Pearl"; break;
    case 35: gift = "Coral"; break;
    case 40: gift = "Ruby"; break;
    case 45: gift = "Sapphire"; break;
    case 50: gift = "Gold"; break;
    case 55: gift = "Emerald"; break;
    case 60: gift = "Diamond"; break;
    default: gift = "No traditional gift"; break;
}

Just like an if statement, a switch statement always has parentheses enclosing some argument. Unlike an if, the argument of a switch must be some kind of data that can be expressed as an integer or a String, not a boolean. For each of the possible values you want the switch to handle, you write a case statement. A case statement consists of the keyword case followed by a constant value, either a literal or a named constant, then a colon. When executed, the JVM jumps to the matching case label and starts executing code there. If there is no matching case label, the JVM goes to the default label. If there is no default label, the entire switch statement is skipped.

One unusual feature of switch statements is that execution falls through case statements. This means that you can use many different case statements for a single segment of executable code. The execution of code in a switch statement jumps out when it hits a break statement. However, break statements are not required, as shown in this switch statement that gives location information for all of the telephone area codes in New York state.

String location = "";
switch(code) {
    case 917: location = "Cellular: ";
    case 212:
    case 347:
    case 646:
    case 718: location += "New York City"; break;

    case 315: location = "Syracuse"; break;

    case 516: location = "Nassau County"; break;

    case 518: location = "Albany"; break;

    case 585: location = "Rochester"; break;

    case 607: location = "South Central New York"; break;

    case 631: location = "Suffolk County"; break;

    case 716: location = "Buffalo"; break;

    case 845: location = "Lower Hudson Valley"; break;

    case 914: location = "Westchester County"; break;

    default:  location = "Unknown Area Code"; break;
}

As you can see, five different area codes are used by New York City. By leaving out the break statements, values of 212, 347, 646, and 718 all have "New York City" stored into location. Area code 917 was originally designated for cellular phones and pagers although now it includes some landlines. By cleverly putting the statement for 917 ahead of the other New York City entries, a value of 917 first stores "Cellular: " into location and then falls through and appends "New York City". For each of these five area codes, execution in the switch statement ends only when the break statement is reached.

The remaining nine area codes are separate. Each of them does a single assignment and then breaks out of the switch block. Finally, the default label is used if the area code doesn’t match one of the given codes. Note that we’ve ordered the (non-NYC) area codes in ascending order for the sake of readability. As shown in the 917 example, there’s no rule about the ordering of the labels. Even the default label can occur anywhere in the switch block you want, although it’s common to put it at the end. Also, the break after the default label is unnecessary because execution exits the switch block anyway. Nevertheless, it’s always wise to end on a break, in the event that you add more cases in later.

Carelessness is always something to watch out for in switch statements. Leaving out a break statement can cause disastrous and difficult to discover bugs. The compiler does not warn you about missing break statements, either. It’s entirely your responsibility to use them appropriately. Because of the dangers involved, it’s often safe to use if-else statements. Any switch statement can be rewritten as some combination of if-else statements, but the reverse is not true. The benefit of switch statements is their ability to list many alternatives clearly. Their drawbacks include the ease of making a mistake, an inability to express ranges of data or most types (double, float, or any reference type other than String), and limited expressive power. They should be used only when their benefit of clearly displaying a list of data outweighs the drawbacks.

Next we give a number of examples to help you get more familiar with switch statements.

Example 4.4 Days in the month

There are fewer uses for switch statements than if statements. Nevertheless, there are problems where their fall-through behavior can be useful. Imagine that you need to write a program that gives the length of each month (with the assumption that February always has 28 days). Given the month as a number, we can use switch statements to write a program that maps the number of the month to the number of days it contains.

Program 4.4 Computes the number of days in a given month.
import java.util.*;

public class DaysInMonth {
    public static void main(String[] args) {                
        Scanner in = new Scanner( System.in );      
        System.out.print("Please a month number (1-12): ");
        int month = in.nextInt();
        int days = 0;
        switch( month ) {
            case 2:  days = 28; break; (1)

            case 4:
            case 6:
            case 9:
            case 11: days = 30; break; (2)

            case 1:
            case 3:
            case 5:
            case 7:
            case 8:
            case 10:
            case 12: days = 31; break; (3)
        }   
        System.out.println("The month you entered has " + days + " days.");
    }
}
1 This program has a single label for February setting days to 28.
2 Then, there are labels for April, June, September, and November, months that each have 30 days.
3 Finally, the large block of January, March, May, July, August, October, and December all set days to 31.

It would be easy to extend this code to prompt the user for a year so that you could integrate the leap year code from above for the February case. Note also that we didn’t use a default label. You might want to set days to some special value (like -1) for invalid months.

In this program it’s necessary to initialize days to some value, in this case 0. Otherwise, the program won’t compile, since it’ll try to print out the value inside days, a value that won’t exist if month is not in the range 1 through 12.

Example 4.5 Ordinal numbers

The term ordinal numbers refers to numbers that are used for ordering within a set of items: first, second, third, and so on. When writing these numbers with numerals in English, it is common to append two letters to the end of the numeral to give the reader a clue that these numerals should be read with their ordinal names: 1st, 2nd, 3rd, and so on.

Unlike most things in English, the rules for deciding which two letters are relatively simple. If the number ends in a 1, the letters “st” should generally be used. If the number ends in a 2, the letters “nd” should generally be used. If the number ends in a 3, the letters “rd” should generally be used. For most other numbers, the letters “th” should be used. We can use a switch statement to write a program to give the correct ordinal endings for most numbers as follows.

Program 4.5 Appends the appropriate suffix to a numeral to make it an ordinal.
import java.util.*;

public class Ordinals {
    public static void main(String[] args) {                
        Scanner in = new Scanner( System.in );      
        System.out.print("Please enter a positive number: ");
        int number = in.nextInt();
        String ending;
        switch( number % 10 ) {
            case 1:  ending = "st"; break;
            case 2:  ending = "nd"; break;
            case 3:  ending = "rd"; break;
            default: ending = "th"; break;
        }   
        System.out.println("Its ordinal version is "
            + number + ending + ".");
    }
}

This program prompts and then reads in an int from the user. We then find the remainder of number when it’s divided by 10, yielding its last digit. Based on this digit, we can pick from the four possibilities and output the correct ordinal number in most cases. Unfortunately, the names for English numbers have an inconsistent naming convention between 11 and 19, inclusive, and the ordinals for any number ending in 11, 12, or 13 will be given the wrong suffix by our code. We leave a more complete solution as an exercise.

Example 4.6 Astrology

Many cultures practice astrology, a tradition that the time of a person’s birth impacts his or her personality or future. One important element of Chinese astrology is their zodiac, consisting of 12 animals. Each consecutive year in a 12-year cycle corresponds to an animal. Because this system repeats, the year one is born in modulo 12 identifies the animal. Below is a table giving these values. For example, if you were born in 1979, 1979 mod 12 ≡ 11; thus, you would be a Ram. Note that this arrangement is based on years in the Gregorian calendar. Chinese astrologers do not list the Monkey as the first animal in the cycle. Note that the names of these animals are also sometimes translated in slightly different ways.

Animal Year
modulo 12
Animal Year
modulo 12

Monkey

0

Tiger

6

Rooster

1

Rabbit

7

Dog

2

Dragon

8

Boar

3

Snake

9

Rat

4

Horse

10

Ox

5

Ram

11

Unfortunately, this table is not very accurate because it’s based on numbering from the Gregorian calendar. The years in question actually start and end based on Chinese New Year, which occurs between January 21 and February 20. As a consequence, you may miscalculate your animal if your birthday is early in the year. Since calculating the date of Chinese New Year is challenging, let’s ignore this problem for the moment and write a program using a switch statement designed to correctly output the animal corresponding to an input birth year.

Program 4.6 Determines a Chinese zodiac animal based on birth year.
import java.util.*;

public class ChineseZodiac {
    public static void main(String[] args) {                
        Scanner in = new Scanner( System.in );      
        System.out.print("Please enter a year: ");
        int year = in.nextInt();        
        String animal = "";
        switch( year % 12 ) {
            case 0:  animal = "Monkey"; break;
            case 1:  animal = "Rooster"; break;
            case 2:  animal = "Dog"; break;
            case 3:  animal = "Boar"; break;
            case 4:  animal = "Rat"; break;
            case 5:  animal = "Ox"; break;
            case 6:  animal = "Tiger"; break;
            case 7:  animal = "Rabbit"; break;
            case 8:  animal = "Dragon"; break;
            case 9:  animal = "Snake"; break;
            case 10: animal = "Horse"; break;
            case 11: animal = "Ram"; break;
        }   
        System.out.println("The Chinese zodiac animal for this year is: " + animal);
    }
}
Sign Symbol Date Range

Aries

The Ram

March 21 to April 19

Taurus

The Bull

April 20 to May 20

Gemini

The Twins

May 21 to June 20

Cancer

The Crab

June 21 to July 22

Leo

The Lion

July 23 to August 22

Virgo

The Virgin

August 23 to September 22

Libra

The Scales

September 23 to October 22

Scorpio

The Scorpion

October 23 to November 21

Sagittarius

The Archer

November 22 to December 21

Capricorn

The Sea-Goat

December 22 to January 19

Aquarius

The Water Bearer

January 20 to February 19

Pisces

The Fishes

February 20 to March 20

In Western astrology, an important element associated with a person’s birth is also called a zodiac sign. The dates for determining this kind of zodiac sign are given by the preceding table.

If you want to implement the rules for this zodiac in code, a switch statement is a good place to start, but you also have to put if statements for each month to test the exact range of dates.

Program 4.7 Determines Western zodiac signs based on birth month and day.
import java.util.*;

public class WesternZodiac {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a month number (1-12): ");
        int month = in.nextInt();
        System.out.print("Please enter a day number in that month (1-31): ");
        int day = in.nextInt();
        String sign = "";
        switch(month) {
            case 1: if(day < 20)
                    sign = "Capricorn";
                else
                    sign = "Aquarius";
                break;
            case 2: if(day < 20)
                    sign = "Aquarius";
                else
                    sign = "Pices";
                break;
            case 3: if(day < 20)
                    sign = "Pices";
                else
                    sign = "Aries";
                break;
            case 4: if(day < 20)
                    sign = "Aries";
                else
                    sign = "Taurus";
                break;
            case 5: if(day < 21)
                    sign = "Taurus";
                else
                    sign = "Gemini";
                break;
            case 6: if(day < 21)
                    sign = "Gemini";
                else
                    sign = "Cancer";
                break;
            case 7: if(day < 23)
                    sign = "Cancer";
                else
                    sign = "Leo";
                break;
            case 8: if(day < 23)
                    sign = "Leo";
                else
                    sign = "Virgo";
                break;
            case 9: if(day < 23)
                    sign = "Virgo";
                else
                    sign = "Libra";
                break;
            case 10:if(day < 23)
                    sign = "Libra";
                else
                    sign = "Scorpio";
                break;
            case 11:if(day < 22)
                    sign = "Scorpio";
                else
                    sign = "Sagittarius";
                break;
            case 12:if(day < 20)
                    sign = "Sagittarius";
                else
                    sign = "Capricorn";
                break;          
        }   
        System.out.println("The zodiac sign is: " + sign);
    }
}

This program is just slightly more complex than the program for the Chinese zodiac. You still need to jump to 12 different cases (numbered 1-12 instead of 0-11), but additional day information is needed to pin down the sign.

4.4. Solution: Monty Hall

We now return to the Monty Hall simulation described at the beginning of the chapter. Recall that objects of the Random class allow us to generate all kinds of random values. To implement this simulation successfully, our program must make the decisions needed to set up the game for the user as well as respond to the user’s input.

import java.util.*; (1)

public class MontyHall {
    public static void main(String[] args) {
        Random random = new Random();
        int winner = random.nextInt(3); (2)
        Scanner in = new Scanner( System.in );
        System.out.print("Choose a door (enter 0, 1, or 2): ");
        int choice = in.nextInt(); (3)
        int alternative; (4)
        int open;
1 We begin with the import statement needed to use both the Scanner and Random class and then define the MontyHall class.
2 In the main() method we first decide which of the three doors is the winner. To do so, we instantiate a Random object and use it to generate a random number that is either 0, 1, or 2 by calling the nextInt() method with an argument of 3. We could have added 1 to this value to get a random choice of 1, 2, or 3, but many counting systems in computer science start with 0 instead of 1. We might as well embrace it.
3 Next, we prompt the user to pick from the three doors and read the choice.
4 Finally, we declare two more int values to keep track of which door to open and which door is the alternative that the user can choose to change over to.

Now, we have to navigate a complicated series of decisions.

        if( choice == winner ) { (1)
            int low;
            int high;
            if( choice ==  0 ) { (2)
                low = 1;
                high = 2;
            }
            else if( choice == 1 ) {
                low = 0;
                high = 2;
            }
            else { //choice == 2
                low = 0;
                high = 1;
            }   
            //randomly choose between other two doors
            double threshold = random.nextDouble(); (3)
            if( threshold < 0.5 ) { (4)
                alternative = low;
                open = high;
            }
            else { (5)
                alternative = high;
                open = low;
            }           
        }
1 In this segment of code, we tackle the possibility that the user happened to choose the winning door. To obey the rules of the game, we must randomly pick which of the two other doors to open.
2 First, we determine which are the other two doors and save them in low and high, respectively.
3 Then, we generate a random number.
4 If the random number is less than 0.5, we keep the lower numbered door as an alternative choice for the user and open the higher numbered door.
5 If the random number is greater than or equal to 0.5, we do the opposite.
        else { (1)
            alternative = winner;
            if( choice == 0 ) { (2)
                if( winner == 1 )                 
                    open = 2;               
                else
                    open = 1;               
            }
            else if( choice == 1 ) {
                if( winner == 0 )                   
                    open = 2;               
                else
                    open = 0;               
            }
            else { //choice == 2 
                if( winner == 0 )                       
                    open = 1;               
                else
                    open = 0;               
            }           
        }
1 This else block covers the case that the player did not pick the winning door the first time. Unlike the previous code segment, we no longer have a choice of which door to open.
2 Instead, we must always make the winner the alternative for the user to pick. Then, we simply determine which door is left over so that we can open it.

Note that the braces surrounding the blocks for each of the braces surrounding the blocks for each of the three possible values of choice are not necessary but are included for readability.

        System.out.println("We have opened Door " + open + (1)
            ", and there is junk behind it!");
        System.out.print("Do you want to change to Door " + (2)
            alternative + " from Door " + choice +
            "? (Enter 'y' or 'n'): ");
        String change = in.next();      
        if( change.equals("y") )
            choice = alternative;
        System.out.println("You chose Door " + choice);
        if( choice == winner ) (3)
            System.out.println("You win a pile of gold!");
        else
            System.out.println("You win a pile of junk.");
    }
}
1 This final segment of code informs the user which door has been opened.
2 It prompts the user to change his or her decision.
3 Depending on the final choice, the program says whether or not the user wins gold or junk.

4.5. Concurrency: Selection

Selection statements (if and switch) seem to have little to do with concurrency or parallelism. Selection allows you to choose between alternatives while concurrency is about the interaction between different threads of execution. As it turns out, there are two reasons why selection and concurrency are deeply related to each other.

The first reason is that selection is one of the most basic tools in Java. It’s impossible to go more than a few lines of a code without encountering a selection statement, usually an if. Concurrent programs are not exempt from this dependence on if statements. Making decisions is at the heart of all programming languages running on all computers.

The second, more troubling reason is related to a problem with some concurrent programs called a race condition, which is discussed in detail in Chapter 14. Remember, one of the biggest challenges of programming a computer is thinking in a completely sequential and logical way. Each line of code is executed one after the other. Adding in if statements means that some code is executed only if a condition is true and skipped otherwise. Consider the following fragment of code:

if(!matches.areLit() && !flyingSparks) {
    storageRoom.enter();
    dynamite.unpack();
}

In this if statement, an imaginary agent only enters the storage room and unpacks the dynamite if the matches are not lit and there are no flying sparks. When execution reaches the first line inside the if block, we are certain that matches.areLit() returned false and flyingSparks is false. This is a one-time check. If the first thing that happens inside the if block is code that lights the matches, Java will not jump out of the if statement.

As always, the programmer is responsible for making an if statement that makes sense. It’s possible that entering the storage room or unpacking the dynamite causes sparks to fly or matches to burst into flames spontaneously, but it seems unlikely. If the storageRoom and dynamite objects were written by other people, we would expect their documentation to explain unusual side-effects of this kind. In a sequential program, the programmer can be reasonably sure that it’s safe to unpack the dynamite.

Consider another fragment of code:

matches.light();
flyingSparks = sparklers.light(matches);

This code appears to light the matches and then to use the lit matches to set some sparklers on fire. Presumably, if the process was successful, flyingSparks will have the value true. This code is reasonable and potentially helpful. If you were celebrating the 4th of July or needed to signal a passing helicopter to rescue you from a desert island, lighting sparklers could be a great idea. This sparkler-lighting code could occur before the dynamite-unpacking code or after it, but, in a sequential program, the protection of the if statement keeps our hero from being blown up if she tries to unpack the dynamite with lit sparklers.

In a concurrent program, all bets are off. Another thread of execution can be operating at the very same time. It’s as if our hero is trying to unpack the dynamite while the villain is lighting sparklers and tossing them into the storage room. If the thread of execution gets to the if statement and makes sure that the matches aren’t lit and that there are no flying sparks, it continues onward. If sparks start flying after that check, it still continues onward, oblivious of the fact. Even though this risk of explosion exists, it depends on the timing of the two (or more) concurrent threads of execution. It might be possible to run a program 1,000 times with no problem. But if the timing is wrong on the 1,001st time, BOOM!

At this point, you don’t need to worry about values inside your if statements being changed by other segments of code, but that problem is at the heart of why concurrent programming can be so difficult. Whether or not you’re programming concurrently, it’s important to keep in mind the assumptions your code makes and the way different parts of your program interact with each other.

4.6. Exercises

Conceptual Problems

  1. Given that x, y, and z are propositions in Boolean logic, make a truth table for the expression ¬(x ∧ ¬y) ⊕ ¬z.

  2. What’s the value of the Boolean expression ¬( (T ⊕ F) ∧ ¬(F ∨ T) )?

  3. The calculation to determine the leap year given in Example 4.1 uses three if statements and three else statements. Write the leap year calculation using a single if and a single else. Feel free to use boolean connectors such as || and &&.

  4. The XOR operator (^) is useful for combining boolean values, but it can be replaced with a more commonly used relational operator in Java. Which one?

  5. De Morgan’s laws are the following, which show that the process of negating a clause changes an AND to an OR and vice versa.

    ¬(x ∧ y) = ¬x ∨ ¬y
    ¬(x ∨ y) = ¬x ∧ ¬y

    Create truth tables to verify both of these statements.

  6. Use De Morgan’s laws given above to rewrite the following statement in Java to an equivalent statement that contains no negations.

    boolean value = !((x != 4) && (y < 2));
  7. Consider the following fragment of code.

    int x = 5;
    int y = 3;
    if(y > 10 && (x = 10) > 5)
        y++;
    System.out.println("x: " + x);
    System.out.println("y: " + y);

    What’s the output? Is the output changed if the condition of the if statement is changed to y > 10 & (x = 10) > 5? Why?

  8. Consider the following fragment of code.

    int a = 7;
    if(a++ == 7)
        System.out.println("Seven");
    else
        System.out.println("Not seven");

    What’s the output? Is the output changed if the condition of the if statement is changed to ++a == 7? Why?
    Note: It is generally wise to avoid increment, decrement, and assignment statements in the condition of an if statement because of the confusion that can arise.

Programming Practice

  1. Write programs that:

    1. Read in two double values and print the larger of the two of them.

    2. Read in three double values and print the largest of the three out. Note: You should use nested if statements.

  2. Write programs that:

    1. Read an int value from the user specifying a certain number of cents. Use if statements to print out the name of the corresponding coin in U.S. currency according to the table below. If the value doesn’t match any coin, print no coin.

      Cents Coin

      1

      penny

      5

      nickel

      10

      dime

      25

      quarter

      50

      half-dollar

      100

      dollar

    2. Read a String value from the user that gives one of the 6 coin names given in the table above. Use if statements to print out the corresponding number of cents for the input. If the name doesn’t many any coin, print unknown coin.

  3. Re-implement both parts from Exercise 4.10 using switch statements instead of if statements.

  4. Expand the program given in Example 4.5 to give the correct suffixes (always “th”) for numbers that end in 11, 12, and 13. Use the modulus operator to find the last two digits of the number. Using an if statement, a switch statement, or a combination, check for those three cases before going into the normal cases.

  5. At the bottom of Section 4.3.3, we use a switch statement to determine the location of various area codes in New York state. Write an equivalent fragment of code using if-else statements instead.

  6. Every member of your secret club has an ID number. These ID numbers are between 1 and 1,000,000 and have two special characteristics: They are multiples of 7 and all end with a 3 in the one’s place. For example, 63 is the smallest such value, and 999,943 is the largest such value. Write a program that prompts the user for an int value, reads it in, and then says whether or not it could be used as an ID number. Note: You need to use the modulus operator in two different ways to test the value correctly.

  7. According to the North American Numbering Plan (NANP) used by the United States, Canada, and a number of smaller countries, a legal telephone number takes the form XYY-XYY-YYYY, where X is any digit 2-9 and Y is any digit 0-9. Write a program that reads in a String from the user and verifies that it is a legal NANP phone number. The length of the entire String must be 12. The fourth and eight characters in the String (with indexes 3 and 7) must be hyphens (-), and all the remaining digits must be in the correct range. Use the charAt() method of the String class to get the char value at each index. Note: There are several ways to structure the if statements you need to use, but the number of conditions may become large. (23 or more!)

  8. Re-implement the solution to the Monty Hall program given in Section 4.4 using JOptionPane to generate GUIs for input and output.

5. Repetition

Q-Tip: You on point, Phife?
Phife Dawg: All the time, Tip.
Q-Tip: You on point, Phife?
Phife Dawg: All the time, Tip.
Q-Tip: You on point, Phife?
Phife Dawg: All the time, Tip.
Q-Tip: Well, then grab the microphone and let your words rip.

— A Tribe Called Quest

5.1. Problem: DNA searching

The world of bioinformatics is the intersection between biology and computer science. Sequencing genomes, determining the function of specific genes, the analysis and prediction of protein structures, and biomedical imaging are just a few of the areas under the umbrella of bioinformatics. Much fascinating research is being done in this area as biologists become better programmers and computer scientists apply their techniques to biology.

Because of its fundamental importance and the incredible amount of information involved, with tens or hundreds of millions of base pairs of DNA in each human chromosome, DNA is a central focus of bioinformatics. As you may know, a DNA strand is made up of a sequence of four nucleotide bases: adenine, cytosine, guanine, and thymine. These bases are usually abbreviated as A, C, G, and T, respectively.

Searching for a specific DNA subsequence within a larger sequence is a common task for biologists to perform. Your goal is to write a program that will search for a subsequence and report how many times it was found within the sequence. For example, if you are given the sequence ATTAGACCATATA and asked to search for CAT, your program should output 1, since there is exactly 1 occurrence of CAT within ATTAGACCATATA. One feature of this problem that makes it more interesting is that occurrences can overlap. For example, given the sequence TATTATTAGATTA and asked to search for TATTA, the correct answer is 2. The sequence begins with a TATTA, but the third T in the sequence is also the first T in a second instance of TATTA.

In Chapter 4, you learned tools that allow you to do comparisons and make choices based on the results. These tools will become even more useful. For example, when you come across a char in a sequence, you know how to compare it to a char in the subsequence you are searching for. The tools you do not yet have are those that allow repetition. Because this problem requires the program to process a DNA sequence of arbitrary length, we will need some way to perform an action repeatedly.

5.2. Concepts: Repetition

You now know how to write choices into a Java program, but so far, each choice can only be made once. So if you want the computer to do a lot of things, you have to type a lot of things. One of the big disadvantages of computers is that they have no intelligence: They can follow instructions blindly, but they can’t do anything else. One of the big advantages of computers is that they’re fast. Modern computers can perform mathematical operations billions of times faster than human beings. To take advantage of this speed, we need to give computers instructions to perform tasks over and over. Such an instruction must have two components to be useful: It must have a way to change the task slightly each time so that each task accomplishes something different. It must also have a way to decide when to stop, otherwise it will continue forever.

The first component is the more subtle one. Crafting a set of instructions so that each repetition of the task is appropriate will be different for every problem. The second component is easier to describe: We’re going to rely on Boolean logic, just as we did for conditional statements. The main tool for repetition in Java and many other languages is called a loop. The body of a loop contains the task to be performed. The rest of the loop, at the very least, contains a condition. Every time the task given in the body of the loop completes, the computer will check the condition. If the condition is true, it’ll do the task again. If the condition is false, the computer is done with the loop and can move on to the code that comes afterward.

One of the difficulties of programming a computer is that we must be very explicit. Even the most obvious tasks must be spelled out in meticulous detail. Let’s consider a simple task, one that we perform every day. If we’re in a room and we want to leave, we simply walk out the nearest door. Assuming there is only one door in the room, how can we describe this process by breaking it down into the steps we (literally) take? Perhaps we could say the following.

Walk toward the door until you reach it.

This statement is a little more specific than Leave the room, but it doesn’t conform nicely to the paradigm of a loop, that is, a clearly separated task and a condition. The following is better.

While you’re not at the door,

take a step toward the door.

Now we have good separation between the work done and the condition for repeating. What’s the task performed in the body of this loop, and what’s the condition? The task is taking a step toward the door. The condition is not being at the door. It seems a little awkward to include that “not,” but in our definition of loops, the body is executed as long as the condition is true.

In a loop, we call each execution of the body of the loop an iteration. When we say that a program iterates over the statements in a loop, we are referring to a single pass through the body of a loop. In this case, the loop will iterate however many times there are steps to the door from the starting position. It is even possible that the loop will iterate zero times: The person following this set of instructions might already be at the door!

It’s hard to get away from numbers in a computer program, especially since everything is fundamentally stored as numbers inside of a computer. So, the most common kind of loop is one that iterates a fixed number of times. For example, your morning exercise routine might include jumping rope 100 times. We could formulate a loop to do that like so.

Set your counter to 0.
While your counter is less than 100,

jump rope.
Increase your counter by 1.

This loop requires set up to start the counter at the right value. Then, the work done by the loop is the actual rope jumping and the counter increment. The condition is the counter being less than 100. Note that this is strictly less than 100. After the first jump, the counter will be incremented to 1. After the 100th jump, the counter will be incremented to 100. Since 100 is not less than 100, the loop will exit. If the condition was the counter being less than or equal to 100, the person following the instructions would jump 101 times.

Input can also be a factor in loop repetitions. For example, you might be a soldier training in the U.S. Marine Corps. Perhaps your drill sergeant has commanded you to do push-ups until he says you can stop. We might formulate a loop to do this as follows.

Do:

Push-up.
Ask the drill sergeant if you can stop.

While the answer is “no.”

As is the case with user input, you must often go into the loop at least once to get the input. This loop requires the soldier to do at least one push-up before asking to stop. Some systems might use input but have other constraints. A more realistic version of this loop might be the following.

Do:

Push-up.
Ask the drill sergeant if you can stop.

While the answer is “no” and you haven’t collapsed.

Remember, the condition for a loop should be a Boolean, and the loop runs as long as the condition is true. However, there is no reason why the Boolean can’t be a complicated expression using all the Boolean logic we have come to know and love.

It’s also possible to nest loops. Nesting loops means putting one loop inside of another, similar to the way that conditional statements could be nested inside of other conditional statements. Just like any other statement, an inner loop will be run as many times as the outer loop runs. Of course, the statements inside of the inner loop will be run according to the conditions of that loop. So, if an outer loop runs 10 times and an inner loop runs 50 times, a statement in the body of an inner loop would run 500 times!

As an example, if you’re working out, you might do several sets of bench presses with a fixed number of reps in each set. If you did 3 sets of 15 bench presses each, your workout program might look like this:

Set your set counter to 0.
While your set counter is less than 3,

set your rep counter to 0.
While your rep counter is less than 15,

do a bench press.
Increase your rep counter by 1.

Rest for 2 minutes.
Increase your set counter.

This way of describing the workout program seems tedious. Most of the description is structural: conditions for the loops and increments for the counters. The only “real” activities are the bench press and the resting. As you can see, the bench press is inside the inner rep loop and will be executed 15 times for each complete execution of the inner rep loop. Since the inner rep loop sits inside the outer set loop, it’ll be executed 3 times, giving a grand total of 45 bench presses. Resting, however, is after the inner rep loop but still contained in the outer set loop and will be executed 3 times, totaling 6 minutes of rest.

As with conditionals, writing out loops in English is tedious and imprecise. In the next section, we’ll discuss the tools for writing loops in Java. Because Java was designed with loops as a central tool, we can write loops more succinctly than in English, squeezing a lot of information into a small space. Because we pack so much information into them, loops can look daunting at first. Remember that the syntax we’ll introduce is only the formal Java way of expressing a condition and a list of instructions to execute repeatedly.

5.3. Syntax: Loops in Java

The Java programming language contains three differently named kinds of loops: while loops, for loops, and do-while loops. All of them allow you to write code that will be executed repeatedly. In fact, any program that uses one kind of loop to solve a problem could be converted to use either of the other two kinds. The three kinds are provided in Java partly so that it’s easy to code certain typical forms of repetition and partly because the C language, an ancestor of Java, contained these three. We’ll begin by describing while loops because they have the simplest form and then move on to the other two kinds. We’ll then explain the syntax for nesting together multiple loops and finally discuss several of the common pitfalls encountered by programmers when coding loops.

5.3.1. while loops

Superficially, the syntax of a while loop resembles an if statement. It starts with the keyword while followed by a boolean condition in parentheses with a block of code surrounded by braces ({ }) afterward. This similarity is not accidental. The only difference between the two is that the body of the if statement will run a only single time, while the body of the while loop will run as long as the condition remains true. Figure 5.1 shows the pattern of execution for while loops.

while
Figure 5.1 If the condition is true, all of the statements in the body of the loop are executed, and then the condition is checked again. When the check is false, execution skips past the body of the loop.

If we assume that the boolean value atDoor says whether or not we have reached the door and the method walkTowardsDoor() allows us to take one step closer to the door, we could formulate our example from the beginning of the previous section as follows.

while(!atDoor) {
    atDoor = walkTowardsDoor();
}

Here we assume that the walkTowardsDoor() method gives back a boolean value that is true if we have reached the door and false otherwise. Unless the walkTowardsDoor() method is able to change the value of atDoor, the loop will repeat forever, a phenomenon known as an infinite loop.

while(true) {
    System.out.println("Help me!");
}

This code gives an example of an infinite loop. If you run this code inside of a program, it’ll print out an endless succession of Help me! messages. Be prepared to stop the program by typing Ctrl+C (hold down the Control key and press C) because it won’t end otherwise. Not all infinite loops are this obvious. A programmer will not usually use true as the condition of a loop, but doing so is not always wrong. Some loops are expected to continue for quite some time with no definite end. To leave a loop abruptly, you can use the break command.

while(true) {
    System.out.println("Help me!");
    break;
}

This loop will only print out a single Help me! before exiting. A break command can be used with an if statement to make a loop that repeats more than once.

int counter = 0;
while(true) {
    System.out.print("the loop ");
    counter++;
    if(counter >= 3)
        break;
}
System.out.println("is on fire!");

This loop will print out the loop the loop the loop is on fire! Of course, the break statement unnecessarily complicates the code. We could have written equivalent code as follows.

int counter = 0;
while(counter < 3) {
    System.out.print("the loop ");
    counter++;
}
System.out.println("is on fire!");

Now, we move on to a more complicated example that can print out the binary representation of a number.

Example 5.1 Binary Conversion

As we discussed in Chapter 1, binary numbers are the building blocks of every piece of data inside a modern computer’s memory. Integers are stored in binary. The representation of floating-point numbers is more complicated, but it also uses 1s and 0s. Even the char data type and the String values built from them are fundamentally stored as binary numbers. For this reason, computer scientists tend to be familiar with the base 2 number system and how to convert between it and base 10, our usual number system.

In base 10, the number 379 is equal to 3 · 100 + 7 · 10 + 9 · 1 = 3 · 102 + 7 · 101 + 9 · 100. Moving from right to left, the value of each place increases by a factor of 10. A binary number is the same, except that the increase is by a factor of 2 and no single digit is greater than 1. Thus, the number 1010112 = 1 · 25 + 0 · 24 + 1 · 23 + 0 · 22 + 1 · 2 + 1 · 20 = 1 · 32 + 0 · 16 + 1 · 8 + 0 · 4 + 1 · 2 + 1 · 0 = 43. In binary, the number 379 = 1011110112.

To convert a number n to binary, we first find the largest power of 2 that is not larger than n. Then, we begin a repetitive process that stops when the power of 2 under consideration is 0. If 2 raised to the current power is bigger than n, we print out a 0 because that power is too big for n. Otherwise, we print out a 1, subtract 2 raised to that power from n, and move on to the next smaller power of 2. This process will print a 0 for every power of 2 that’s not in n and a 1 for every one that is, giving exactly the definition of a number written in base 2.

Program 5.1 Outputs a binary representation of a decimal number.
import java.util.*;

public class DecimalToBinary {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a base 10 number: ");
        int number = in.nextInt();
        int power = 1;
        while(power <= number/2)
            power *= 2;
        while(power > 0) {
            if(power > number)
                System.out.print(0);
            else {
                System.out.print(1);
                number -= power;
            }
            power /= 2;
        }
    }
}

The first while loop in this program doubles the value of power until doubling it again would make it larger than number. We go up to and including number/2, otherwise we’d stop when power was larger than number. After that loop, we begin repeatedly checking to see if a given power of 2 is bigger than the value left in number. If it is, we know that we do not use that power. If it’s not, we do and must remove that power from the value of number.

You may have been tempted to solve this problem by determining if a given number is even or odd. If it’s even, then you record a 0, and if it’s odd, then you record a 1. You could then divide the number by two and repeat the process of determining whether it is even or odd. You would continue this process until the number became 0. This procedure requires only a single while loop and would give the digits of the number in base 2. Unfortunately, you would get the digits in reverse order. Because we write our numbers with the most significant digit on the left, we had to use the code given above to first find the largest value and work backward, in order to determine the binary digits in the correct sequence.

5.3.2. for loops

Let’s return to our code that prints out the loop the loop the loop is on fire!

int counter = 0;
while(counter < 3) {
    System.out.print("the loop ");
    counter++;
}
System.out.println("is on fire!");

This code involves some initialization, a condition, and an update, as many loops do. The initialization sets counter to 0, the condition checks to make sure that counter is less than 3, and the update increments counter by 1 every iteration of the loop. These three elements are so common that a special kind of loop called the for loop was designed with them explicitly in mind. Most for loops are dependent on a single counting variable. To make the loop easy to read, the initialization, condition, and update, all of which relate to this variable, are pulled into the header of the loop. We could code the previous while loop example more cleanly, using a for loop, as follows.

for(int i = 0; i < 3; i++) {
    System.out.print("the loop ");
}
System.out.println("is on fire!");

The header of a for loop consists of those three parts: the initialization, the condition, and the update, all separated by semicolons. Figure 5.2 shows the pattern of execution for for loops.

for
Figure 5.2 The loop is initialized. If the condition is true, all of the statements in the body of the loop are executed, followed by the increment step. Then the condition is checked again. When the check is false, execution skips past the body of the loop.

You may have noticed that we changed the variable name used within the loop from counter to i. Doing so doesn’t change the function of the code. We did so because using the variables i, j, and sometimes k is a very common practice with for loops. By using variables named like this, we are indicating that the variable is just a dummy counter that we are using to make the loop work, not some variable with a grander purpose. Also, with three uses of a single variable in the header of a for loop, a long variable name takes up a lot of space.

for loops are used in Java programs more than the other two loops. They work well when you know how many times you want to iterate through the loop, which you often do. You can think of the first part of the for loop header as the starting point, the second part as the ending point, and the third part as how you get from the start to the end. Many beginning programmers get stuck on the idea that every for loop starts with int i = 0 and ends with i++. While this pattern is often true, there are many other ways to use a for loop. For example, we could print the powers of 2 that are less than 1000.

for(int i = 1; i < 1000; i *= 2) {
    System.out.println(i);
}

This segment of code prints out 1, 2, 4, 8, 16, 32, 64, 128, 256, and 512 on separate lines, which are the powers of 2 from 20 up to 29.

Both of the examples of for loops we have given have only had a single executable line in the body of the loop. Like if statements, loops only require braces if their bodies have more than one executable line. Many of the while loops from the previous subsection could have been written without braces.

Just because a for loop already has a counting mechanism doesn’t mean that we won’t need other variables to perform useful tasks. For example, given a String, we could try to find the letter of the alphabet in the String which is closest to the end of the alphabet. For the String "Pluto is no longer a planet", the latest letter in the alphabet is 'u'. To write code that will do this job, we must use the counting variable from the for loop as an index into the String. Then, we must also have a temporary variable where we keep the latest letter found so far. To get the ith char from a String, we can use the charAt() method. Recall that the index of the first char in a String is 0, and the index of the last char is one less than the length of the String.

String s = "The quick brown fox jumps over the lazy dog.";
String lower = s.toLowerCase();
char latest = ' ';
char c;
for(int i = 0; i < lower.length(); i++) {
    c = lower.charAt(i);
    if(c >= 'a' && c <= 'z' && c > latest)
        latest = c;
}
System.out.println("The latest character in the alphabet from your message is: '"
	+ latest + "'.");

The first thing we do in this example is convert s to lowercase, so that we are comparing all char values in the same case. Next, we run through lower, starting at index 0 and going until we reach the end of the String. For each char, we check to see if it is an alphabetic character and then if it is later in the alphabet than our current latest. If it is, we store it into latest. After the loop, we print out the value in latest. We have chosen the char ' ' because it is numerically earlier than all the letters in the alphabet. If the output is a space, we’d know that none of the characters in s were alphabetic.

For the example given, the latest character in the alphabet is 'z' because of the word "lazy". One weakness in this code is that it will always search through the entire String, even if the letter 'z' has already been found. For the String "The quick brown fox jumps over the lazy dog.", we’re not wasting too much time. However, if the String were "Zanzibar!" followed by the full text of War and Peace, we’d be wasting thousands and thousands of operations reading characters when we knew that 'z' was going to be the latest letter, no matter what. We can rewrite our for loop so that it quits early if it reaches a 'z'.

for(int i = 0; i < lower.length(); i++) {
    c = lower.charAt(i);
    if(c >= 'a' && c <= 'z' && c > latest)
        latest = c;
    if(latest == 'z')
        break;
}

This version of the for loop will break out immediately if the latest is already a 'z'. This code will work efficiently, but many professional programmers discourage the use of break except when absolutely necessary (like in a switch statement). If a break is used to exit the loop, this logic can be encoded into the condition of the loop. Thus, the same loop written with better style would be the following.

for(int i = 0; i < lower.length() && latest != 'z'; i++) {
    c = lower.charAt(i);
    if(c >= 'a' && c <= 'z' && c > latest)
        latest = c;
}

For this final version of the loop, we have made the conditional portion of the header more complex. The comparison using < gives a boolean that we combine using && with the boolean from the comparison using !=. As always, remember that the loop will continue iterating as long as the condition is true. Since we need both parts of the condition to be true to continue executing, we use the && operator to connect them.

We apologize to international readers for focusing on the Latin alphabet used by English and many other Western European languages. It should be possible to make a localized version of this example with any alphabet by checking the return value of Character.isLetter(c), which is valid for all single-character Unicode values, although the idea of alphabetical order doesn’t really apply to some character systems like the hanzi and kanji of Chinese and Japanese. Regardless, using the Character.isLetter() method is recommended for almost all applications, since it’s more general and more readable.

Example 5.2 Primality testing

Prime numbers are integers greater than 1 whose only factors are 1 and themselves. If you’ve encountered prime numbers before, they probably seemed like a mathematical curiosity and nothing more. In fact, prime numbers are the basis of a very practical application of mathematics: cryptography. With the use of some math and very large prime numbers, computer scientists have devised techniques that make messages sent over the Internet more secure.

These techniques are beyond the scope of this book, but we can at least write some code to determine if a number n is prime. To do so, we can simply divide n by all the numbers between 2 and n - 1. If none of the numbers divide it evenly, it must be prime. Here is this basic solution.

Program 5.2 Gives a naive approach for testing if a number is prime.
import java.util.*;

public class PrimalityTester0 {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a number: ");
        long number = in.nextLong();        
        boolean prime = true;
        for(long i = 2; i < number && prime; i++)
            if(number % i  ==  0)
                prime = false;
        if(prime)
            System.out.println(number + " is prime.");
        else
            System.out.println(number + " is not prime.");
    }
}

This program has a for loop that runs from 2 up to number - 1, provided that we don’t find a number that evenly divides number. This optimization means that the program will output the moment that it knows that the number is not prime, but we’ll have to wait for it to check all the other possibilities before it is sure that the number is prime.

One insight that we can use to make the program more efficient is that, after checking 2, we don’t have to divide it by any even numbers. So, we can do half the checking with a few simple modifications.

Program 5.3 Gives a slightly cleverer approach for testing if a number is prime.
import java.util.*;

public class PrimalityTester1 {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a number: ");
        long number = in.nextLong();        
        boolean prime = number == 2 || number % 2 != 0;      
        for(long i = 3; i < number && prime; i += 2 )
            if(number % i  ==  0)
                prime = false;
        if(prime)
            System.out.println(number + " is prime.");
        else
            System.out.println(number + " is not prime.");
    }
}

This version of the program sets the boolean variable prime to false if number is divisible by 2 (unless it’s 2 itself) and true otherwise. Then, it starts the search at 3 and continues in jumps of 2. Although we save half the checks, we can do much better. Note that if a number n is divisible by 2, then it’s also divisible by n/2. So, if a number is not divisible by 2, it’s also not divisible by any number larger than n/2. If it’s not divisible by 2 or 3, then it’s also not divisible by any number larger than n/3. If it’s not divisible by 2 or 3 or 4, it’s not divisible by any number larger than n/4, and so on. Thus, we don’t have to check all the way up to n - 1. If we’re checking to see if n is divisible by x and learning that n is not divisible by anything larger than n/x, the point where x = n/x is as follows.

root

Thus, we only need to search up to the square root of n, which will save much more time.

Program 5.4 Gives a much faster approach for testing if a number is prime.
import java.util.*;

public class PrimalityTester2 {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a number: ");
        long number = in.nextLong();        
        boolean prime = number == 2 || number % 2 != 0;      
        long root = (long)Math.sqrt(number);
        for(long i = 3; i <= root && prime; i += 2 )
            if(number % i  ==  0)
                prime = false;
        if(prime)
            System.out.println(number + " is prime.");
        else
            System.out.println(number + " is not prime.");
    }
}

Note in this version of the program we do go up to and including root, because there’s the possibility that number is a perfect square.

Example 5.3 DNA reverse complement

DNA is usually double stranded, with each base paired to another specific base, called its complementary base. The following table shows the association between each base and its complementary base.

Base Abbreviation Complementary Base

Adenine

A

T

Cytosine

C

G

Guanine

G

C

Thymine

T

A

A simple but common task is finding the reverse complement of a DNA sequence. The reverse complement of a DNA sequence is its sequence of complementary bases given in reverse order. For example, the reverse complement of ACATGAG is CTCATGT. This sequence is found by first finding the complement of ACATGAG, which is TGTACTC, and then reversing its order.

We’ll write a program that finds the reverse complement of a DNA sequence entered by a user. This sequence will be entered as a sequence of characters made up of the four abbreviations for the bases: A, C, G, and T. We’ll store this sequence as a String and perform some manipulations on it to get the reverse complement.

Program 5.5 Finds the reverse complement of a DNA sequence.
import java.util.*;

public class ReverseComplement {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a DNA sequence: ");
        String sequence = in.next().toUpperCase();              
        String complement = "";
        for(int i = 0; i < sequence.length(); i++) 		(1)
            switch(sequence.charAt(i)) { //get complements
                case 'A': complement += "T"; break;
                case 'C': complement += "G"; break;
                case 'G': complement += "C"; break;
                case 'T': complement += "A"; break;
            }       
        String reverseComplement = "";
        //reverse the complement
        for(int i = complement.length() - 1; i >= 0; i--)	(2)
            reverseComplement += complement.charAt(i);
        System.out.println("Reverse complement: " + reverseComplement);
    }
}
1 This example first creates a String filled with the complement of the base pairs from the input String.
2 Then, it creates a new String that is the reverse of the complement sequence.

Note how complement is created by appending the char corresponding to the complementary base at the end of complement. If we inserted each char at the beginning of complement, we wouldn’t need to reverse in a separate step.

Program 5.6 More cleverly finds the reverse complement of a DNA sequence.
import java.util.*;

public class CleverReverseComplement {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("Please enter a DNA sequence: ");
        String sequence = in.next().toUpperCase();              
        String reverseComplement = "";
        for(int i = 0; i < sequence.length(); i++)
            switch(sequence.charAt(i)) { //get complements
                case 'A': reverseComplement = "T" + reverseComplement; break;
                case 'C': reverseComplement = "G" + reverseComplement; break;
                case 'G': reverseComplement = "C" + reverseComplement; break;
                case 'T': reverseComplement = "A" + reverseComplement; break;
            }               
        System.out.println("Reverse complement: " + reverseComplement);
    }
}

5.3.3. do-while loops

Use this rule of thumb for deciding which kind of loop to use: If you know how many times you want the loop to execute, use a for loop. If you don’t know how many times you want it to execute, use a while loop. Clearly, this rule is not iron-clad. In the previous example, we used a for loop even though it would stop executing as soon as a 'z' was encountered. Nevertheless, it seems like we have covered all of the possible situations with while and for loops. When should we use do-while loops? The simple answer is: never.

You never have to use a do-while loop. With a little bit of effort, you could use a single kind of loop for every job. The key difference between a do-while loop and a regular while loop is that a do-while loop will always run at least once. Neither of the other two loops give you that guarantee. The syntax for a do-while loop is a do at the top of a loop body enclosed in braces, with a normal while header at the end, including a condition in parentheses, followed by a semicolon. Figure 5.3 shows the pattern of execution for do-while loops.

dowhile
Figure 5.3 The statements in the body of the loop are executed, and then the condition is checked. When the check is false, execution skips past the body of the loop. A do-while loop is guaranteed to run at least once.

We can use a do-while loop to print out the first 10 perfect squares as follows.

int x = 1;
do {
    System.out.println(x*x);
    x++;
} while(x <= 10);

This loop behaves exactly the same as the following loop.

int x = 1;
while(x <= 10) {
    System.out.println(x*x);
    x++;
}

The time when a do-while loop is really going to shine is when your program will work incorrectly if the loop doesn’t run at least once. This situation often occurs with input, when the loop must run at least once before checking the condition. For example, imagine that you want to write a program that picks a random number between 1 and 100 and lets the user guess what it is until the user gets it right. You need a loop because it’s a repetitive activity, but you need to let the user guess at least once so that you can check if he or she was right. The following program fragment does exactly that.

Scanner in = new Scanner(System.in);
Random random = new Random();
int guess;
int number = random.nextInt(100) + 1;
do {
    System.out.print("What's your guess? ");
    guess = in.nextInt();
} while(guess != number);
System.out.println("You got it! The number was " + number + ".");

You could perform the same function with a while loop, but you’d need to get input from the user before the loop starts. Using the do-while loop is a little more elegant.

5.3.4. Nested loops

Just as you can nest if statements, it’s possible to nest loops inside of other loops. In the simplest case, you may have some repetitive activity that itself needs to be performed several times. For example, when you were younger, you probably had to learn your multiplication tables. For each number, a multiplication table gave the value of the product of that number by every integer between 1 and 12. We can write code to print out out the multiplication table for every number from 1 to 10 by simply repeating the process.

for(int number = 1; number <= 10; number++) {
    for(int factor = 1; factor <= 12; factor++) {
        System.out.println(number + " x " + factor +
            " = " + number*factor);
    }
    System.out.println();
}

The outer loop incrementing number runs 10 times. The inner loop incrementing factor will run 12 times for each iteration of the outer loop. Thus, the code in the inner loop will run a total of 120 times. Every 12 iterations, the inner loop will stop, and an extra blank line will be added by the System.out.println() method in the outer loop.

Example 5.4 Triangular numbers

The sequence consisting of 1, 3, 6, 10, 15, and so on is known as the triangular numbers. The ith triangular number is the sum of the first i integers. They are called triangular numbers because they can be drawn as equilateral triangles in a very natural way, if you use a number of dots equal to the number.

We can use nested loops to print out the first n triangular numbers, where n is specified by the user.

Program 5.7 Prints out triangular numbers.
import java.util.*;

public class TriangularNumbers {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("How many triangular numbers? ");
        int n = in.nextInt();
        int sum;                        
        for(int i = 1; i <= n; i++) {
            sum = 0;            
            for(int j = 1; j <= i; j++)
                sum += j;
            System.out.println(sum);
        }
    }
}

As you can see, the outer loop iterates through each of the n different triangular numbers. Then, the inner loop does the summation needed to compute the given triangular number. However, producing a sequence of triangular numbers this way is inefficient. Nested loops are an effective way to solve many problems, particularly certain types of problems using arrays, but we can generate triangular numbers using only a single for loop. The key insight is that we can keep track of the previous triangular number and add i to it, as i increases.

Program 5.8 Prints out triangular numbers more cleverly.
import java.util.*;

public class CleverTriangularNumbers {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in);      
        System.out.print("How many triangular numbers? ");
        int n = in.nextInt();
        int triangular = 0;                     
        for(int i = 1; i <= n; i++) {         
            triangular += i;
            System.out.println(triangular);
        }
    }
}

By removing the inner for loop, the total amount of work needed is greatly reduced.

5.3.5. Common pitfalls

With great power comes great responsibility. The power to repeat things a large number of times means that we can also repeat our mistakes a large number of times. Many classic bugs occur as a result of logical or typographical errors in loops. We list a few of the most common below.

Pitfall: Infinite loops

It’s possible to create a loop that never terminates. Your program may take a long time to finish, but if it takes much longer than you expect, an infinite loop might be the culprit. Infinite loops might occur because you forgot to include an appropriate statement to advance a counter.

int number = 1;
while(number <= 100)
    System.out.println(number);
}

This code is presumably intended to print out the first 100 integers, but there’s no code that increases the value of number. As a consequence, the number 1 will be printed out over and over until the user stops the program from executing. Usually, the cause is more subtle, as in the following code.

for(int i = 0; i < 10; i += 0.5)
    System.out.println("Half a step forward, half a step back...");

One might expect this code to print out 20 lines of output. However, remember that i is an int. Adding 0.5 to 0 and then casting it to an int gives 0 again. What’s particularly insidious about this loop is that it compiles without even a warning in Java. Usually conversion from a double to an int requires an explicit cast, but the += operator (and other similar operators) behave a little differently for technical reasons.

Pitfall: Almost infinite loops

Many loops are truly infinite; others take a really long time. For example, if you intended to run a loop down from 10 to 0, but increment your counter instead of decrementing it, overflow means that you will eventually get to a number less than 0, but it will take more than 2 billion increments instead of the expected 10 decrements.

for(int i = 10; i > 0; i++)
    System.out.println(i);
System.out.println("Blast off!");

This loop will significantly slow your code. Everyone will be so tired of waiting that they might leave the rocket launch. Of course, another problem with almost infinite loops is that you are dealing with the wrong values. No one expects to hear the number 2,147,483,647 in a countdown.

Pitfall: Fencepost errors

Perhaps the most common loop errors are fencepost errors, often known as off-by-one errors. The name “fencepost” comes from a related mistake that someone might make when putting up a fence. Imagine that you want to erect a 10 meter long chain link fence with a support post every meter, how many posts do you need? In fact, we haven’t given you enough information to answer the question correctly. If your fence is built in a straight line, you’ll need 11 posts so that you have a post at each end. However, if your fence is a rectangular enclosure, say 3 meters by 2 meters, you’ll only need 10 posts.

In loops, fencepost errors are often due to zero-based counting. A for loop that iterates 10 times is below.

for(int i = 0; i < 10; i++)
    System.out.println(i);

Of course, sometimes we need one-based counting instead. After being used to zero-based counting, a programmer might make the following loop that incorrectly iterates 9 times.

for(int i = 1; i < 10; i++)
    System.out.println(i);

The correct version that iterates 10 times is below.

for(int i = 1; i <= 10; i++)
    System.out.println(i);

If you want to iterate n times, start at 0 and go up to but not including n or alternately start at 1 and go up to and including n. To keep loop headers consistent, some programmers always start at 0 and then adjust the values inside the loop, printing out i + 1 in this case.

Pitfall: Skipped loops

A loop runs as long as its condition is true. For for loops and while loops, this could mean that the loop is never even entered. Sometimes, that behavior is intended by the programmer. Sometimes, the programmer made a mistake.

For example, we can write a program that will add any number of positive values. When the user is finished using the adder, he or she enters a negative number. This negative number, called a sentinel value, tells the program to stop executing the loop. Below is an incorrect implementation of such a program.

Scanner in = new Scanner(System.in);
int number = 0;
int sum = 0;
while(number > 0) {
    sum += number;
    System.out.print("Enter the next number to add: ");
    number = in.nextInt();
}
System.out.println("The total sum is " + sum);

This loop will never be executed because 0 is not greater than 0. The program could be changed by making the condition of the while loop number >= 0. Doing so will allow the user to enter 0 as input, which is fine since it doesn’t change the value of the sum. If you want to force the user to enter only numbers greater than zero, you could change the loop into a do-while loop.

Pitfall: Misplaced semicolons

The idea of a statement in Java is often amorphous in the minds of beginning programmers. An entire loop (with any number of loops nested inside of it) is considered one statement. An executable statement ending with a semicolon is one statement as well, even when that executable statement is empty. Thus, the following is a legal (but infinite) loop.

int i = 100;
while(i > 0); {
    System.out.println(i);
    i--;
}
System.out.println("Ready or not, here I come!");

This code was supposed to count down from 100, just like in the game of Hide and Seek; however, there is a semicolon after the condition of the while loop. This semicolon is treated like an executable statement that does nothing. As a consequence, the while loop does the single statement, checks if the condition is true (which it is), and continues to do the empty statement and check the condition, forever. The extra braces enclose two statements unnecessarily, but Java allows extra braces, as long as they are evenly matched.

This error is common especially for those new to loops and conditional statements and are in the habit of putting semicolons after everything. A misplaced semicolon doesn’t always result in an infinite loop. Here is the for loop version of the same code, also with a semicolon inserted after the loop header.

int i;
for(i = 100; i > 0; i--); {
    System.out.println(i);
}
System.out.println("Ready or not, here I come!");

This version of the code will execute similarly, except the decrement is built into the header of the loop. So, the loop will execute the empty statement, but it will also decrement i. This code will decrement i 100 times, then print out 0 exactly once, then print Ready or not, here I come!.

There are some cases when an empty statement for a loop body is useful although it is never necessary. In future chapters, we’ll point out situations in which you may wish to use an empty statement this way.

5.4. Solution: DNA searching

Below we give a solution to the DNA searching problem posed at the beginning of the second half of this chapter. Our solution prints out the index within the sequence when it finds a match with the subsequence it’s looking for. Afterward, it prints out the total number of matches. Our code also does error checking to make sure that the user only enters valid DNA sequences containing the letters A, C, G, and T. We begin our code with the standard import statement and class definition.

import java.util.*;

public class DNASearch {
    public static void main(String[] args) {                
        Scanner in = new Scanner(System.in); (1)
        String sequence, subsequence;
        boolean valid; (2)
        char c;
1 The main() method instantiates a Scanner object and declares both of the String variables we’ll need to store the DNA sequences.
2 The method also declares a boolean and a char we’ll use for input checking.
        do {
            System.out.print("Enter the DNA sequence you wish to search in: "); (1)
            sequence = in.next().toUpperCase(); (2)
            valid = true;
            for(int i = 0; i < sequence.length() && valid; i++) { (3)
                c = sequence.charAt(i);
                if(c != 'A' && c != 'C' && c != 'G' && c != 'T') { (4)
                    System.out.println("Invalid DNA sequence!");
                    valid = false;
                }
            }
        } while(!valid); (5)
1 Next, the user is prompted for a DNA sequence to search in.
2 This String stored in sequence is converted to uppercase just in case the user is not being consistent.
3 The inner for loop in this code checks each char inside of sequence.
4 If any char is not an 'A', 'C', 'G', or 'T', then valid is set to false. As a result, the for loop terminates. Also, the do-while loop repeats the prompt and gets a new String for sequence from the user.
5 This outer do-while loop continues as long as the user keeps entering invalid DNA sequences.
        do {        
            System.out.print("Enter the subsequence you wish to search for: ");
            subsequence  = in.next().toUpperCase();
            valid = true;
            for(int i = 0; i < subsequence.length() && valid; i++) {                
                c = subsequence.charAt(i);
                if(c != 'A' && c != 'C' && c != 'G' && c != 'T') {
                    System.out.println("Invalid DNA sequence!");
                    valid = false;
                }
            }
        } while(!valid);

The code used to input subsequence while doing error checking is virtually identical to the code to input sequence.

        int found = 0;
        for(int i = 0; i < sequence.length() - subsequence.length() + 1; i++) { (1)
            for(int j = 0; j < subsequence.length(); j++) { (2)
                if(subsequence.charAt(j) != sequence.charAt(i + j)) (3)
                    break;
                if(j == subsequence.length() - 1) { //matches (4)
                    System.out.println("Match found at index " + i);
                    found++;
                }
            }
        }
1 The workhorse of the search is found in these nested for loops. The outer loop iterates through every index in sequence, until it comes to an index that is too late to be the start of a new subsequence (since the subsequence would be too long to fit anymore). This happens to be when the value of i is greater than or equal to sequence.length() - subsequence.length() + 1. It may take some thought to verify that this condition is the correct one. One way to think about this problem is by noting that, when sequence and subsequence have the same length, you need to check starting at index 0 of sequence but not any later indexes. Also, if subsequence is one char longer than sequence, there can never be a match. In that case, the value of sequence.length() - subsequence.length() + 1 would be 0. Since 0 is not less than 0, the outer for loop would never execute.
2 The inner for loop iterates through the length of subsequence, making sure that every char in sequence, starting at the appropriate offset, exactly matches a char in subsequence.
3 If, at any point, the two char values do not match, the inner for loop will immediately exit, using the break command.
4 However, on the last iteration of the inner for loop, when j is one less than the length of subsequence, we know that all of subsequence matched a part of sequence. As a result, we print out the index of sequence where subsequence started and increment the found counter.

If you know the String class well, you can use the indexOf() method to replace the inner for loop. We leave that approach as an exercise.

Finally, we print out the total number of matches found. In order to avoid awkward output like 1 matches found., we used an if-else to customize the output based on the value of found.

        if(found == 1)
            System.out.println("One match found.");
        else
            System.out.println(found + " matches found.");
    }
}

The ideas needed to correctly implement the solution are not difficult, but catching all the off-by-one errors and getting every detail right takes care. There’s also more than one way to code this solution. For example, we could have written the nested loops that do the searching as follows.

int found = 0;
for(int i = 0; i < sequence.length() - subsequence.length() + 1; i++) {
    for(int j = 0; j < subsequence.length() &&
        subsequence.charAt(j) == sequence.charAt(i + j); j++)
        if(j == subsequence.length() - 1) { // Matches
            System.out.println("Match found at index " + i);
            found++;
        }
    }
}

This design is preferred by many since it removes the break. By using an empty statement, it’s possible to move the check to see if the matching process is done outside of the inner for loop.

int found = 0;
int j;
for(int i = 0; i < sequence.length() - subsequence.length() + 1; i++) {
    for(j = 0; j < subsequence.length() &&
        subsequence.charAt(j) == sequence.charAt(i + j); j++);
    if(j == subsequence.length()) { // Matches
        System.out.println("Match found at index " + i);
        found++;
    }
}

In this case, note that we must declare j outside of the inner for loop, since it will be used outside. This approach is more efficient because we only need to perform the check once. Note that the condition of the if statement has also changed. Now, we know that all of subsequence matches because the loop ran to completion. If the loop did not run to completion, then j would be smaller than subsequence.length() and the loop must have terminated because the two char values did not match. Although more efficient, some programmers would avoid this approach because it uses confusing syntax in which the body of the for loop is a single empty statement followed by a semicolon. Likewise, the logic about exiting the loop and the condition of the if statement is murkier.

5.5. Concurrency: Loops

Many programmers use concurrency for speedup, to make their programs to run faster. Most programs that run for a long time use loops to do repetitive tasks. If these loops are doing the same operation to many different pieces of data, we may be able to speed up the process by splitting up the data and letting different threads operate on their own segment of the data. Splitting up data this way is called domain decomposition which allows us to achieve data parallelism. These topics are discussed further in Section 13.3.

Performing repetitive tasks is one of the great strengths of computers. For most programs that run a long time, incredible amounts of computation are being done inside of (usually nested) loops. Domain decomposition will not work for all of these programs. Some cannot be parallelized at all, but this book is about finding problems that can have parallel and concurrent solutions.

In Chapter 13, we’ll introduce tools for writing a concurrent program with different threads of execution running at the exactly the same time and potentially interacting. Using only the power of loops, you can see parallelism in action now.

Example 5.5 Parallelism without threads

Consider the problem of computing the sum of the sines of a range of integers. At its heart is a loop from the start of the range to the end.

for(int i = start; i <= end; i++)
    sum += Math.sin(start);

If we want to allow the user to specify the start and the end and print out the sum, we need to make a program with a little bit of input and output around this loop.

Program 5.9 Adds the sines of all integers in a range specified by the user.
import java.util.Scanner;

public class SumSines {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("Enter starting value: ");
        int start = in.nextInt();
        System.out.print("Enter ending value: ");
        int end = in.nextInt();
        
        double sum = 0;
        for(int i = start; i <= end; i++)     
            sum += Math.sin(start);
        
        System.out.println("Sum of sines: " + sum);
    }
}

If you compile and run this program with 1 as the start value and 100000000 as the end, the answer should be 1.7136493465700542. One hundred million values is a lot to find the sine for. Depending on your machine, this task should take between 10 seconds and over a minute. Try to time how long this takes as accurately as possible.

Now, open a total of four terminal windows and navigate them all to the directory with SumSines.class in it. Run SumSines in each one. For the first terminal, enter 1 as the start and 25000000 as the end. For the second, enter 25000001 and 50000000. For the third, enter 50000001 and 75000000. For the last, enter 75000000 and 100000000. Once they have run, you should get, respectively, 1.4912473269134603, -0.6795491754132104, -0.2893142602684644, and 1.1912654553381272. If you add these together using a calculator, you should get 1.7136493465699127, which is almost exactly the same answer we got before. (Floating-point rounding errors cause the slight difference.)

If you try to start them computing at about the same time, you can try to see how long it takes for all of them to complete. Did it take less time than before? If you have a single core processor, it might have taken just as long or longer. If you have a dual-core processor, it should have taken less time, and if you have a quad core processor, even less. Since we’re dividing the problem into four pieces, we don’t expect to see any improvement with more than four cores.

Most operating systems provide a graphical way of viewing the load on each processor. If you examine your CPU usage while running those programs, you should see it spike up when the programs start and then come down when they finish. For multiple cores, how did we say which core we wanted each program to run on? We didn’t. In general, it’s difficult to specify which core we want to run a program, process, or thread on. The OS does the job of scheduling and picks a free processor when it needs to run a program. It’s even possible for programs and threads to change from one core to another while running if the OS needs to balance out the workload.

This sines example is similar to Example 13.10 in Chapter 13. As you may have noticed, running four programs simultaneously is not convenient. You have to open several windows, you have to type starting and ending points very carefully, and you have to combine the answers at the end since your programs cannot interact directly with each other. Features of Java will make this job easier, allowing us to run more than one thread of execution at a time without the need to run multiple programs by hand.

5.6. Exercises

Conceptual Problems

  1. If you have a String containing a long text and you want to count the number of words in the text that begin with the letter 'm', which of the three kinds of loops would you use, and why?

  2. In Example 5.2, our last version of the primality tester PrimalityTester2 computes the square root of the number being tested. Instead of computing this value before the loop, how would performance be affected by changing the head of the for loop to the following?

    for(long i = 3; i <= Math.sqrt(number) && prime; i += 2)
  3. How many different DNA sequences of length n are there?

  4. There are three different errors in the following loop intended to print out the numbers 1 through 10. What are they?

    for(int i = 1; i < 10; i--);
    {
        System.out.println(i);
    }
  5. Consider the following code containing nested for loops.

    Scanner in = new Scanner(System.in);
    int n = in.nextInt();
    int count = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= i; j++)
            count++;

    In terms of the value of n, how many times is count incremented? If it’s not immediately obvious, trace through the execution of the program by hand or run the code for several different values of n and try to detect a pattern.

Programming Practice

  1. Write a program that converts base 10 numbers into base 3 numbers. If you find that task too easy, write a program that will convert base 10 numbers to any base in the range 2 to 16. Hint: Use letters A through Z, in order, to represent digits larger than 9.

  2. The greatest common divisor (GCD) of two integers is the largest integer that divides both of them evenly. The GCD for any two positive integers is at least 1 and at most the smaller of the two numbers. Write a program that prompts a user for two int values and finds their GCD. Although there are more efficient methods, you can count down from either number. If the counter ever divides both numbers evenly, it’s the GCD. The counter is guaranteed to divide them both if it reaches 1.

  3. In the solution to the DNA searching problem given in Section 5.4, we used two for loops to find occurrences of a DNA subsequence inside of a larger sequence. Professional Java developers would have used a single for loop and the indexOf() method in the String class. One version of this method returns the index of a substring within a String object, starting from a particular offset, as shown below.

    String text = "fun dysfunction";
    String search = "fun";
    System.out.println("Location: " + text.indexOf(search, 4));

    This code will output Location: 7 since the first occurrence of "fun" from index 4 or later starts at index 7. If there are no more occurrences of the substring beyond the starting index, the method will return -1. Rewrite the solution to the DNA searching problem, replacing the inner searching for loop with the indexOf() method.

  4. Write a program that reads a number n from a user and then prints all possible DNA sequences of length n. Be careful not to supply too large of a value when you run this program. Hint: Represent the sequence as a String. On each iteration, focus on the last char in the String. If it is an 'A', change it to a 'C'. If it is a 'C', change it to a 'G'. If it is a 'G', change it to a 'T'. If it is a 'T', change it back to an 'A', but “carry” the increment over to the next char, like a rolling odometer. You will have to design loops that can deal with carries that cascade across multiple indexes.

  5. Re-implement the solution to the DNA searching program given in Section 5.4 using JOptionPane to generate GUIs for input and output.

Experiments

  1. Using a for loop, record the Monty Hall simulation so that you can run it 100 times, always choosing to switch doors. Keep a record of how many times you win. Change your code again to run the Monty Hall simulation 100 more times, always choosing to keep your initial choice. Again, keep a record of how many times you win. Compare the two records. Choosing to switch should perform roughly twice as well as sticking with your initial choice. Increase the number of iterations to 1,000 and then 10,000 times. Does the performance of switching get closer to twice the performance of not switching?

  2. Write three nested for loops, each of which run 1,000 times. Increment a counter in the innermost for loop. If that counter starts at 0, its final value should be 1,000,000,000. Time how long your program takes to run to completion using either a stopwatch or, if you’re on a Unix or Linux system, the time command. Feel free to increase and decrease the amount that each loop runs to see the effect on the time. However, if you increase the values of all three loops too much, you may have to wait a long while.

  3. In Section 5.3.5, one of the common loop mistakes we discuss is an almost infinite loop. Create your own almost infinite loop that runs from 10 to 0, incrementing instead of decrementing. Time the execution of your program. Unlike our example, do not use an output statement or your code will take too long to run. How much longer would your code take to run if you used a long instead of an int?

  4. In Example 5.2, we gave three programs to test a number for primality. Run each of these prime testers on a large prime such as 982,451,653 and time them. Is there a significant difference in the running time of PrimalityTester0 and PrimalityTester1? What about PrimalityTester1 and PrimalityTester2?

  5. In Example 5.5, we ran four programs at the same time to solve a problem in parallel. Use the same framework (combined with your knowledge of primes from Example 5.2) to write a program that can see how many prime numbers are in a user specified range of integers. Then, use it to find the total number of primes between 2 and 500,000,000. Now, run two copies of the program with one starting at 2 and going up to 250,000,000 and the other starting at 250,000,001 and going up to 500,000,000. If you add the numbers together, do you get the same answer? (If not, there is a bug in your program.) Now, divide the work into four pieces. How much quicker, if at all, is running all four programs instead of one? Does one of the four pieces run significantly faster or slower than the others?

6. Arrays

Too much of a good thing can be wonderful.

— Mae West

6.1. Introduction

With one exception, all of the types we’ve talked about in this book have held a single value. For example, an int variable can only contain a single int value. If you try to put a second int value into a variable, it will overwrite the first.

The String type, of course, is the exception. String objects can contain char sequences of any length from 0 up to a practically limitless size (the theoretical maximum length of a Java String is Integer.MAX_INT, more than 650 times the length of War and Peace). As remarkable as String objects are, this chapter is about a more general kind of list called an array. We can create arrays to hold a list of any type of variable in Java.

The ability to work with lists expands the scope of problems that we can solve. Beyond simple lists, we can use the same tools to create tables, grids, and other structures to solve fascinating problems like the one that comes next.

6.2. Problem: Game of Life

Some physicists insist that the rules governing the universe are horribly complicated. Some insist that the fundamental laws are simple and only their overall interaction is complex. With the power to do simulations quickly, computer scientists have shown that some systems can exhibit very complex interactions using simple rules. Perhaps the best known examples of these systems are cellular automata, of which Conway’s Game of Life is the most famous.

The framework for Conway’s Game of Life is an infinite grid made up of squares called cells. Some cells in the grid are black (or alive, to give it a biological flavor), and the rest are white (or dead). Any given cell has 8 neighbors as shown in the figure below.

cell
Figure 6.1 Cell (shown in gray) surrounded by 8 neighboring cells.

The pattern of alive and dead cells at any given time on the grid is called a generation. To determine the next generation, we use the following rules.

  1. If a living cell has fewer than two living neighbors, it dies from loneliness.

  2. If a living cell has more than three neighbors, it dies because of overcrowding.

  3. If a living cell has exactly two or three living neighbors, it lives to the next generation.

  4. If a dead cell has exactly three live neighbors, it becomes a living cell.

These four simple rules allow for more complex interactions than you might expect. The patterns that emerge from applying these rules to a starting configuration of alive and dead cells strike a balance between complete chaos and rigid order. As the name of the game implies, the similarity to biological patterns of development can be surprising.

Your problem is to create a Life simulator of size n × m, specific values for which will be discussed below. The program should simulate the process at a speed that is engaging to watch, with a new generation every tenth of a second. The program should begin by randomly making 10% of all the cells living and the rest dead.

6.2.1. Terminal limitations

One problem you might be worrying about is how to display the simulation. In Chapter 15 you’ll learn how to make a graphical user interface (GUI) that can display a grid of cells in black and white and much more interesting things as well. For now, the main tool that we can use for output is still the terminal. The output method we recommend is printing an asterisk ('*') for a living cell and a space (' ') for a dead one. In this way you can easily see the patterns form on the screen and change over time.

The classic terminal isn’t very big. For this reason, we suggest that you set the height of your simulation to 24 rows and the width of your simulation to 80 columns. These dimensions conform to the most ancient terminal sizes. If your terminal screen is much larger, you can change the width and height later to perform a larger simulation. No matter how large the display for the game is, the ideal size of the game. Because our size is so limited, we must deal with the problem of a cell on the boundary. Anything beyond the boundaries should be counted as dead. Thus, a cell right on the edge of the simulation grid can have a maximum of 5 neighbors. A cell in one of the four corners can only have 3 neighbors.

In order to give the appearance of smooth transitions, you need to print out each new generation of cells quickly, and in the same locations as the previous generation. Simply printing one set after another will not achieve this effect unless your terminal screen is exactly 24 rows tall. So, you will need to clear the screen each time. In an effort to be platform independent, Java does not provide an easy way to clear the terminal screen. A quick and simple hack is to simply print out 100 blank lines before printing the next generation. This hack will work as long as your terminal is not significantly more than 100 rows in height. If it is, you’ll need to print a larger number of blank rows.

Finally, you need to wait a short period of time between generations so that the user can see each configuration before it’s cleared away and replaced by the next one. The simplest way to do this is by having your program go to sleep for a short period of time, a tenth of a second as we suggested before. The code to make your program sleep for that amount of time is:

try { Thread.sleep(100); }
catch(InterruptedException e) {}

We’ll explain this code in much greater detail in Chapter 13. The key item of importance is the number passed into the sleep() method. This value is the number of milliseconds you want your program to sleep. 100 milliseconds is, of course, one tenth of a second.

In order to simulate the Game of Life, we need to store information, namely the liveness or deadness of cells, in a grid. First, we need to discuss the simpler task of storing data in a list.

6.3. Concepts: Lists of data

Lists of data are of paramount importance in many different areas of life: shopping lists, packing lists, lists of employees working at a company, address books, top ten lists, and more. Lists are even more important in programming. As you know, one of the great strengths of computers is their speed. If we have a long list of data, we can use that speed to perform operations on all the data quickly. E-mail contact lists, entries in a database, and cells in a spreadsheet are just a few of the most obvious ways that lists come up in computer applications.

Even in Java, there are many different ways to record a list of information, but a list is only one form of data structure. As the name implies, a data structure is a way to organize data, whether in a list, in a tree, in sorted order, in some kind of hierarchy, or in any other way that might be useful. We’ll only talk about the array data structure in this chapter, but other data structures will be discussed in Chapter 18. Below, we give a short explanation of some of the attributes any given data structure might have.

6.3.1. Data structure attribute

Contents

Keeping only a single value in a data structure defeats the purpose of a data structure. But, if we can store more than a single value, must all of those values come from the same type? If a data structure can hold several different types of data, we call it heterogeneous, but if it can only hold one type, we call it homogeneous.

Size

The size of a data structure may be something that’s fixed when it’s created or it could change on the fly. If a data structure’s size or length can change, we call it a dynamic data structure. If the data structure has a size or length that can never change after it’s been created, we call it a static data structure.

Element Access

One of the reasons there are so many different data structures is that different ways of structuring data are better or worse for a given task. For example, if your task is to add a list of numbers, then you’re expecting to access every single element in the list. However, if you’re searching for a word in a dictionary, you don’t want to check every dictionary entry to find it.

Some data structures are optimized so that you can efficiently read, insert, or delete only a single item, often the first (or last) item in the data structure. Some data structures only allow you to move through the structure sequentially, one item after another. Such a data structure has what is called sequential access. Still others allow you to jump randomly to any point you want inside the data structure efficiently. These data structures have what is called random access. Advanced programmers take into account many different factors before deciding which data structure is best suited to their problem.

6.3.2. Characteristics of an array

Now that we’ve defined these attributes, we can say that an array is a homogeneous, static data structure with random access. An array is homogeneous because it can only hold a list of a single type of data, such as int, double, or String. An array is static because it has a fixed length that is set only when the array is instantiated. An array also has random access because jumping to any element in the array is fast and takes about the same amount of time as jumping to any other.

An array is a list of a specific type of elements that has length n, a length specified when the array is created. Each of the n elements is indexed using a number between 0 and n - 1]. Once again, zero-based counting rears its ugly head. Consider the following list of items: {9, 4, 2, 1, 6, 8, 3}

If this list is stored in an array, the first element, 9, would have index 0, 4 would have index 1, and so on, finishing at 3 with an index of 6, although the total number of items is 7. Not all languages use zero-based counting for array indexes, but many do, including C, C++, and Java. The reason that languages like C originally used zero-based counting for indexes is that the variable corresponding to the array is an address inside the computer’s memory giving the first element in the array. Thus, an index of 0 is 0 times the size of an element added to the starting address, and an index of 5 is 5 times the size of an element added to the starting address. So, zero-based indexes gave a quick way for the program to compute where in memory a given element of an array is.

6.4. Syntax: Arrays in Java

The idea of a list is not mysterious. Numbering each element of the list is natural, even though the numbers start at 0 instead of 1. Nevertheless, arrays are the source of many errors that cause Java programs to crash. Below, we explain the basics of creating arrays, indexing into arrays, and using arrays with loops. Then, there’s an extra subsection explaining how to send data from a file to a program as if the file were being typed in by a user. Using this technique can save a lot of time when you’re experimenting with arrays.

6.4.1. Array declaration and instantiation

To create an array, you usually need to create an array variable first. Remember that an array is a homogeneous data structure, meaning that it can only store elements of a single type. When you create an array variable, you have to specify what that type is. To declare an array variable, you use the type it’s going to hold, followed by square brackets ([]), followed by the name of the variable. For example, if you want to create an array called numbers that can hold integers, you would type the following.

int[] numbers;

If you have some C or C++ programming experience, you might be used to the brackets being on the other side of the variable, like so.

int numbers[];

In Java, both declarations are perfectly legal and equivalent. However, the first declaration is preferred from a stylistic perspective. It follows the pattern of using the type (an array of int values in this case) followed by the variable name as the syntax for a declaration.

As we said, arrays are also static data structures, meaning that their length is fixed at the time of their creation. Yet we didn’t specify a length above. This declaration has not yet created an array, just a variable that can point at an array. In the second half of this chapter, we will further discuss this difference between the way an array is created and the way an int or any other variable of primitive type is created. To actually create the array, we need to use another step, involving the keyword new. Here’s how we instantiate an array of int type with 10 elements.

numbers = new int[10];

We use the keyword new, followed by the type of element, followed by the number of elements the array can hold in square brackets. This new array is stored into numbers. In other words, the variable numbers is now a name for the array. Commonly, the two steps of declaring and instantiating an array will be combined into one line of code.

int[] numbers = new int[10];

It’s always possible to separate the two steps. In some cases, a single variable might be used to point at an array of one particular length, then changed to point at an array of another length, and so on, as below.

int[] numbers;
numbers = new int[10];
numbers = new int[100];
numbers = new int[1000];

Here, the variable numbers starts off pointing at no array. Next, it’s made to point at a new array with 10 elements. Then, it’s made to point at a new array with 100 elements, ignoring the 10 element array. Finally, it’s made to point at an array with 1,000 elements, ignoring the 100 element array. Remember, the arrays themselves are static; their lengths can’t change. The array type variables, however, can point at different arrays with different lengths, provided that they’re still the right type (in this case, arrays of int values).

What values are inside the array when it’s first created? Let’s return to the case where numbers points at a new array with 10 elements. Each of those elements contains the int value 0, as shown below.

array
Figure 6.2 Array elements showing index values.

Whenever an array is instantiated, each of its n elements is set to some default value. For int, long, short, and byte this value is 0. For double and float, this value is 0.0. For char, this value is '\0', a special unprintable character. For boolean, this value is false. For String or any other reference type, this value is null, a special value that means there’s no object.

It’s also possible to use a list to initialize an array. For example, we can create an array of type double that contains the values 0.5, 1.0, 1.5, 2.0, and 2.5 using the following code.

double[] increments = {0.5, 1.0, 1.5, 2.0, 2.5};

This line of code is equivalent to using the new keyword to create a double array with 5 elements and then setting each to the values shown.

6.4.2. Indexing into arrays

To use a value in an array, you must index into the array, using the square brackets once again. Returning to the example of the int array numbers with length 10, we can read the value at index 4 from the array and print it out.

System.out.println(numbers[4]);

Until some other value has been stored there, the value of numbers[4] is 0, and so 0 is all that will be printed out. We can set the value at numbers[4] to 17 as follows.

numbers[4] = 17;

Then, if we try to print out numbers[4], 17 will be printed. The contents of the numbers array will look like this.

array2
Figure 6.3 Array showing contents of elements.

The key thing to understand about indexing into an array is that it gives you an element of the specified type. In other words, numbers[4] is an int variable in every possible sense. You can read its value. You can change its value. You can pass it into a method. It can be used anywhere a normal int can be used, as in the following example.

int x = numbers[4];
double y = Math.sqrt(numbers[2]) + numbers[4];
numbers[9] = (int)(y*x);

Executing this code will store 17 into x and 17.0 into y. Then, the product of those two, 289, will be stored into numbers[9]. Remember, in Java, the type on the left and the type on the right of the assignment operator (=) must match, except in cases of automatic casting, like storing an int value into a double variable. Since they have the same type, it makes sense to store an element of an int array like numbers[4] into an int variable like x. However, an array of int values can’t be stored into an int type.

int z = numbers;

This code will cause a compiler error. What would it mean? You can’t put a list of variables into a single variable. And the converse is true as well.

numbers = 31;

This code will also cause a compiler error. A single value can’t be stored into a whole list. You would have to specify an index where it can be stored. Furthermore, you must be careful to specify a legal index. No negative index will ever be legal, and neither will an index greater than or equal to the number of elements in the array.

numbers[10] = 99;

This code will compile correctly. If you remember, we instantiated the array that numbers points at to have 10 elements, numbered 0 through 9. Thus, we are trying to store 99 into the element that is one index after the last legal element. As a result, Java will cause an error called an ArrayIndexOutOfBoundsException to happen, which will crash your program.

6.4.3. Using loops with arrays

One reason to use arrays is to avoid declaring 10 separate variables just to have 10 int values to work with. But once you have the array, you’ll often need an automated way to process it. Any of the three kinds of loops provides a powerful tool for performing operations on an array, but the for loop is an especially good match. Here is an example of a for loop that sets the values in an array to their indexes.

int[] values = new int[100];
for(int i = 0; i < 100; i++)
    values[i] = i;

This sample of code shows how easy it is to iterate over every element in an array with a for loop, but it has a flaw in its style. Note that the number 100 is used twice: once in the instantiation of the array and a second time in the termination condition of the for loop. This fragment of code works fine, but if the programmer changes the length of values to be 50 or 500, the bounds of the for loop will also need to change. Furthermore, the length of the array might be determined by user input.

To make the code both more robust and readable, we can use the length field of the values array for the bound of the for loop.

int[] values = new int[100];
for(int i = 0; i < values.length; i++)
    values[i] = i;

The length field gives the length of the array that values points to. If the programmer wants to instantiate the array with a different length, that’s fine. The length field will always reflect the correct value. Whenever possible, use the length field of arrays in your code. Note that the length field is read-only. If you try to set values.length to a specific value, your code will not compile.

Setting the values in an array is only one possible task you can perform with a loop. Let’s assume that an array of type double named data has been declared, instantiated, and filled with user input. We could sum all its elements using the following code. A more elegant way to do the same summation is discussed in Section 6.9.1.

double sum = 0.0;
for(int i = 0; i < data.length; i++)
    sum += data[i];
System.out.println("The sum of your data is: " + sum);

So far, we’ve only discussed operations on the values in an array. It is important to realize that the order of those values can be equally important. We’re going to create an array of char type named letters, initialized with some values, and then reverse the order of the array.

char[] letters = {'b', 'y', 'z', 'a', 'n', 't', 'i', 'n', 'e'}; (1)
int start = 0; (2)
int end = letters.length - 1; (3)
char temp;
while(start < end) { (4)
    temp = letters[start]; (5)
    letters[start] = letters[end];
    letters[end] = temp;
    start++; (6)
    end--;
}
for(int i = 0; i < letters.length; i++) (7)
    System.out.print(letters[i]);
1 After initializing the letters array, we declare start and end.
2 start gets the value 0, the first index of letters.
3 end gets the value letters.length - 1, the last valid index of letters.
4 The while loop continues as long as the start is less than the end.
5 The first three lines of each iteration of the while loop will swap the char at index start with the char at index end.
6 The two lines after that will increment and decrement start and end, respectively. When the two meet in the middle, the entire array has been reversed.
7 The simple for loop at the end prints out each char in letters, giving the output enitnazyb.

Of course, we could have printed out the array elements in reverse order without changing their order, but we wanted to reverse them, perhaps because we will need them reversed in the future.

6.4.4. Redirecting input

With arrays and loops, we can process a lot of data, but testing programs that process a lot of data can be tedious. Instead of typing data into the terminal, we can read data from a file. In Java, file I/O is a messy process that involves several objects and method calls. We’re going to talk about it in depth in Chapter 20, but for now we can use a quick and easy workaround.

If you create a text file using a simple text editor, you can redirect the file as input to a program. Everything you’ve written in the text file is treated as if it were being typed into the command line by a person. To do so, you type the command using java to run your class file normally, type the < sign, and then type the name of the file you want to use as input. For example, if you have a text file called numbers.txt that you want to use as input to a program stored in Summer.class, you could do so as follows.

java Summer < numbers.txt

Redirecting input this way is not a part of Java. Instead, it’s a feature of the terminal running under your OS. Not all operating systems support input redirection, but virtually every flavor of Linux and Unix do, as well as the Windows command line and the macOS terminal. We could write the program mentioned above and give it the simple task of summing all the numbers it gets as input.

Program 6.1 Sums a list of numbers given as input.
import java.util.*;

public class Summer {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("How many numbers do you want to add? ");
        int n = in.nextInt();
        int sum = 0;
        for(int i = 0; i < n; i++) {
            System.out.print("Enter next number: ");
            sum += in.nextInt();
        }   
        System.out.println("The sum of the numbers is " + sum);
    }
}

Now, we can type out a file with a list of numbers in it and save it as numbers.txt. To conform with the program we wrote, we should also put the total count of numbers as the first value in the file. You can put each number on a separate line or just leave a space between each one. As long as they are separated by white space, the Scanner object will take care of the rest. You’ll have to type the numbers into the file once, but then you can test your program over and over with the same file.

If you do run the program with the file you’ve created, you’ll notice that the program still prompts you once for the total count of numbers and then prompts you many times to enter the next number. With redirected input, all that text runs together in a bizarre way. All the input is coming from numbers.txt. If you expect a program to read strictly from redirected input, you can design your code a little differently. For one thing, you don’t need to have explicit prompts for the user. For another, you can use a number of special methods from the Scanner class. The Scanner class has a several methods like hasNextInt() and hasNextDouble(). These methods will examine the input and see if there is another legal int or double and return true or false accordingly. If you expect a file to have only a long sequence of int values, you can use hasNextInt() to determine if you’ve reached the end of the file or not. Using hasNextInt(), we can simplify the program and remove the expectation that the first number gives the total count of numbers.

Program 6.2 Sums a list of numbers given as input without prompting the user.
import java.util.*;

public class QuietSummer {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int sum = 0;
        while(in.hasNextInt())
            sum += in.nextInt();        
        System.out.println("The sum of the numbers is " + sum);
    }
}

On the other hand, you might be interested in the output of a program. The output could be very long or it might take a lot of time to produce or you might want to store it permanently. For these situations, it is possible to redirect output as well. Instead of printing to the screen, you can send the output to a file of your choosing. The syntax for this operation is just like the syntax for input redirection except that you use the > sign instead of <. To run QuietSummer with input from numbers.txt and output to sum.txt, we could do the following.

java QuietSummer < numbers.txt > sum.txt

You would be free to examine sum.txt at any time with your text editor of choice. When using output redirection, it makes more sense to run QuietSummer than Summer. If we had run Summer, all of that unnecessary output prompting the user to enter numbers would be saved in sum.txt.

6.5. Examples: Array usage

Here are a few examples of practical array usage. We’re going to discuss some techniques useful mostly for searching and sorting. Searching for values in a list seems mundane, but it’s one of the most practical tasks that a computer scientist routinely carries out. By making a computer do the work, it saves human beings countless hours of tedious searching and checking. Another important task is sorting. Sorting a list can make future searches faster and is the simplest way to find the median of a list of values. Sorting is a fundamental part of countless real world problems.

In the examples below, we’ll first discuss finding the largest (or smallest) value in a list, move on to sorting lists, and then talk about a task that searches for words, like a dictionary look up.

Example 6.1 King of the hill

Finding the largest value input by a user is not difficult. Applying that knowledge to an array is pretty straightforward as well. This simple task is also a building block of the sorting algorithm we’ll discuss below. The key to finding the largest value in any list is to keep a temporary variable that records the largest value found so far. As we go along, we update the variable if we find a larger value. The only trick is initializing the variable to some appropriate starting value. We could initialize it to zero, but what if entire list of numbers is negative? Then, our answer would be larger than any of the numbers in the list. If our list of numbers is of type int, we could initialize our variable to Integer.MIN_VALUE, the smallest possible int. This approach works, but you have to remember the name of the constant, and it doesn’t improve the readability of the code.

When working with an array, the best way to find the largest value in the list is by setting your temporary variable to the first element (index 0) in the array. Below is a short snippet of code that finds the largest value in an int array named values in exactly this way.

int largest = values[0];
for(int i = 1; i < values.length; i++)
    if(values[i] > largest)
        largest = values[i];
System.out.println("The largest value is " + largest);

Note that the for loop starts at 1 not 0. Because largest is initialized to be values[0], there’s no reason to check that value a second time. Doing so would still give the correct answer, but it wastes a tiny amount of time.

What’s the feature of this code that makes it find the largest value? The key is the > operator. With the change of a single character, we could find the smallest value instead.

int smallest = values[0];
for(int i = 1; i < values.length; i++)
    if(values[i] < smallest)
        smallest = values[i];
System.out.println("The smallest value is " + smallest);

In addition to the necessary change from > to <, we also changed the output and the name of the variable to avoid confusion. Now, we’ll show how repeatedly finding the smallest value in an array can be used to sort it. Alternatively, the largest value could be used equally well.

Example 6.2 Selection sort

Sorting is the bread and butter of computer scientists. Much research has been devoted to finding the fastest ways to sort a list of data. The rest of the world assumes that sorting a list of data is trivial because computer scientists have done such a good job solving this problem. The name of the sorting algorithm we are going to describe below is selection sort. It’s not one of the fastest ways to sort data, but it’s simple and easy to understand.

The idea behind selection sort is to find the smallest element in an array and put it at index 0 of the array. Then, from the remaining elements, find the smallest element and put it at index 1 of the array. The process continues, filling the array up from the beginning with the smallest values until the entire array is sorted. If the length of the array is n, we’ll need to look for the smallest element in the array n - 1 times. By putting the code that searches for the smallest value inside of an outer loop, we can write a program that does selection sort of int values input by the user as follows. This program’s not very long, but there’s a lot going on.

import java.util.*;

public class SelectionSort {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt(); (1)
        int[] values = new int[n]; (2)
        int smallest;
        int temp;
        for(int i = 0; i < values.length; i++) (3)
            values[i] = in.nextInt();
1 After instantiating a Scanner, we read in the total number of values the list will hold. We cannot rely on the hasNextInt() method to tell us when to stop reading values.
2 We need to know up front how many values we are going to store so that we can instantiate our array with the appropriate length.
3 Then, we read each value into the array using the first for loop.
        for(int i = 0; i < n - 1; i++) { (1)
            smallest = i;
            for(int j = i + 1; j < n; j++) (2)
                if(values[j] < values[smallest])
                    smallest = j; (3)
            temp = values[smallest]; (4)
            values[smallest] = values[i];
            values[i] = temp;
        }
1 The next for loop is where the actual sort happens. We start at index 0 and then try to find the smallest value to be put in that spot. Then, we move on to index 1, and so on, just as we described before. Note that we only go up to n - 2. We don’t need to find the value to put in index n - 1, because the rest of the list has the n - 1 smallest numbers in it and so the last number must already be the largest.
2 If you look carefully, you’ll notice that the inner for loop has the same overall shape as the loop used to find the smallest value in the previous example; however, there is one key difference:
3 Instead of storing the value of the smallest number in smallest, we now store the index of the smallest number. We need to store the index of the smallest number so that, in the next step, we can swap the corresponding element with the element at i, the spot in the array we’re trying to fill.
4 The three lines after the inner for loop are a simple swap to do exactly that.
        System.out.print("The sorted list is: ");
        for(int i = 0; i < values.length; i++) (1)
            System.out.print(values[i] + " ");
    }
}
1 After all the sorting is done, the final for loop prints out the newly sorted list.

This program gives no prompts for user input, so it’s well designed for input redirection. If you’re going to make a file containing numbers you want to sort with this program, make sure that the first number is the total count of numbers in the file.

Again, this program sorts the list in ascending order (from smallest to largest). If you wanted to sort the list in descending order, you would only need to change the < to a > in the comparison of the inner for loop, although other changes are recommended for the sake of readability.

Example 6.3 Word search

In this example, we read in a list of words and a long passage of text and keep track of the number of times each word in the list occurs in the passage. This kind of text searching has many applications. Similar ideas are used in a spell checker that needs to look up words in a dictionary. The incredibly valuable find and replace tools in modern word processors use some of the same techniques.

To make this program work, however, we need to read in a (potentially long) list of words and then a lot of text. We are forced to use input redirection (or some other file input) because typing this text in multiple times would be tedious. When we get to Chapter 20, we’ll talk about ways to read from multiple files at the same time. Right now, we can only redirect input from a single file, and so we’re forced to put the list of words at the top of the file, followed by the text we want to search through.

import java.util.*;

public class WordCount {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt(); (1)
        String[] words = new String[n]; (2)
        int[] counts = new int[n]; (3)
        String temp;    
        for(int i = 0; i < words.length; i++) (4)
            words[i] = in.next().toLowerCase();
1 As in the last example, this program begins by reading in the length of the list of words.
2 Then, it instantiates the String array words to hold these words.
3 It also instantiates an array counts of type int to keep track of the number of times each word is found. By default, each element in counts is initialized to 0.
4 The first for loop in the program reads in each word and stores it into the array words.
        while(in.hasNext()) { (1)
            temp = in.next().toLowerCase();         
            for(int i = 0; i < n; i++) (2)
                if(temp.equals(words[i])) {
                    counts[i]++; (3)
                    break;  
                }           
        }
        System.out.println("The word counts are: ");
1 The while loop reads in each word from the text following the list and stores it in a variable called temp.
2 Then, it loops through words and tests to see if temp matches any of the elements in the list.
3 If it does, it increases the value of the element of counts that has the same index and breaks out of the inner for loop.
        for(int i = 0; i < words.length; i++) (1)
            System.out.println(words[i] + " " + counts[i]);
    }
}
1 After all the words in the text have been processed, the final for loop prints out each word from the list, along with its counts.

This program uses two different arrays for bookkeeping: words contains the words we are searching for and counts contains the number of times each word has been found. These two arrays are separate data structures. The only link between them is the code we wrote to maintain the correspondence between their elements.

To give a clear picture of how this program should behave, here’s a sample input file with two paragraphs from the beginning of The Count of Monte Cristo by Alexandre Dumas.

7
and
at
bridge
for
pilot
vessel
walnut
On the 24th of February, 1815, the look-out at Notre-Dame de la Garde signaled the three-master, the Pharaon from Smyrna, Trieste, and Naples. As usual, a pilot put off immediately, and rounding the Chateau d'If, got on board the vessel between Cape Morgion and Rion island.

Immediately, and according to custom, the ramparts of Fort Saint-Jean were covered with spectators; it is always an event at Marseilles for a ship to come into port, especially when this ship, like the Pharaon, has been built, rigged, and laden at the old Phocee docks, and belongs to an owner of the city.

And here’s the output one should get from running WordCount with input redirected from the file given above.

The word counts are:
and 6
at 3
bridge 0
for 1
pilot 1
vessel 1
walnut 0

For this example, the program works fine. However, our program would have given incorrect output if ship, spectators, or several other words in the text had been on the word list. You see, the next() method in the Scanner class reads in String values separated by white space. The word ship appears twice in the text, but the second instance is followed by a comma. Since the words are separated by white space only, the String "ship," does not match the String "ship". Dealing with punctuation is not difficult, but it would increase the length of the code, so we leave it as an exercise.

Example 6.4 Statistics

Imagine you’re a teacher who has just given an exam. You want to produce statistics for the class so that the students have some idea how well they have done. You want to write a Java program to help you produce the statistics, to save time now and in the future.

The statistics you want to collect are listed in the following table.

Statistic Description

Maximum

Maximum score

Minimum

Minimum score

Mean

Average of all the scores

Standard Deviation

Sample standard deviation of the scores

Median

Middle value of the scores when ordered

Example 6.1 covered how to find the maximum and minimum scores in a list. The mean is simply the sum of all the scores divided by the total number of scores. Standard deviation is a little bit trickier. It gives a measurement of how spread out the data is. Let n be the number of data points, label each data point xi, where 1 ≤ i ≤ n, and let μ be the mean of all the data points. Then, the formula for the sample standard deviation is as follows.

deviation

Finally, if you sort a list of numbers in order, the median is the middle value in the list, or the average of the two middle values, if the list has an even length.

These kinds of statistical operations are useful and are packaged into many important business applications such as Microsoft Excel. This version will have a simple interface whose input comes from the command line. First, the total number of scores will be entered. Then, each score should be entered one by one. After all the data has been entered, the program should compute and output the five values.

Below we give the solution to this statistics problem. Several different tasks are combined here, but each of them should be reasonably easy to solve after the previous examples.

import java.util.*;

public class Statistics {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt(); (1)
        int[] scores = new int[n]; (2)
        for(int i = 0; i < n; i++) (3)
            scores[i] = in.nextInt();
1 In our solution, the main() method begins by reading in the total number of scores.
2 It declares an int array of that length named scores.
3 Then, we read in each of the scores and store them into scores.
        int max = scores[0]; (1)
        int min = scores[0];
        int sum = scores[0];
        for(int i = 1; i < n; i++) { (2)
            if(scores[i] > max)
                max = scores[i];
            if(scores[i] < min)
                min = scores[i];
            sum += scores[i];
        }
1 Here we declare variables max, min, and sum to hold, respectively, the maximum, minimum, and sum of the elements in the array. Then, we set all three variables to the value of the first element of the array. These initializations make the following code work.
2 In a single for loop, we find the maximum, minimum, and sum of all the values in the array.

We could have done so with three separate loops, but this approach is more efficient. Setting max and min to scores[0] follows the pattern we’ve used before, but setting sum to the same value is also necessary in this case. Because the loop iterates from 1 up to scores.length - 1, we must include the value at index 0 in sum. Alternatively, we could have set sum to 0 and started the for loop at i = 0.

        double mean = ((double)sum)/n;
        System.out.println("Maximum:\t\t" + max);
        System.out.println("Minimum:\t\t" + min);
        System.out.println("Mean:\t\t\t" + mean);

In this short snippet of code, we compute the mean, being careful to cast it into type double before the division, and then print out the three statistics we’ve computed.

        double variance = 0;
        for(int i = 0; i < n; i++)
            variance += (scores[i] - mean)*(scores[i] - mean); (1)
        variance /= (n - 1); (2)
        System.out.println("Standard Deviation:\t" + Math.sqrt(variance)); (3)
1 At this point, we use the mean we’ve already computed to find the sample standard deviation. Following the formula for sample standard deviation, we subtract the mean from each score, square the result, and add it to a running total. Although the formula for sample standard deviation uses the bounds 1 to n, we translate them to 0 to n - 1 because of zero-based array numbering.
2 Dividing the total by n - 1 gives the sample variance.
3 Then, the square root of the variance is the standard deviation.
        int temp;
        for(int i = 0; i < n - 1; i++) { (1)
            min = i;
            for(int j = i + 1; j < n; j++)
                if(scores[j] < scores[min])
                    min = j;
            temp = scores[min];
            scores[min] = scores[i];
            scores[i] = temp;
        }
1 To find the median, we use our selection sort code.

Note that we have reused the variable min to hold the smallest value found so far, instead of declaring a new variable such as smallest. Some programmers might object to doing so, since we run the risk of interpreting the variable as the minimum value in the entire array, as it was before. Either approach is fine. If you worry about confusing people reading your code, add a comment.

        double median;
        if(n % 2 == 0) (1)
            median = (scores[n/2] + scores[n/2 + 1])/2.0; (2)
        else
            median = scores[n/2]; (3)
        System.out.println("Median:\t\t\t" + median);
    }
}
1 After the array has been sorted, we need to do a quick check to see if its length is odd or even.
2 If its length is even, we need to find the average of the two middle elements.
3 If its length is odd, we can report the value of the single middle element.

Note that some of the statistics we found, such as the maximum, minimum, or mean, could be computed with a single pass over the data without the need for an array for storage. However, the last two tasks need to store all the values to work. The simplest way to find the sample standard deviation of a list of values requires its mean, requiring one pass to find the mean and a second to sum the squares of the difference of the mean and each value. Likewise, it’s impossible to find the median of a list of values without storing the list.

6.6. Concepts: Multidimensional lists

In the previous half of the chapter, we focused on lists of data and how to store them in Java in arrays. The arrays we have discussed already are one-dimensional arrays. That is, each element in the array has a single index that refers to it. Given a specific index, an element will have that index, come before it, or come after it. These kinds of arrays can be used to solve a huge number of problems involving lists or collections of data.

Sometimes, the data needs to be represented with more structure. One way to provide this structure is with a two-dimensional array. You can think of a two-dimensional array as a table of data. Instead of using a single index, a two-dimensional array has two indexes. Usually, we think about these dimensions as rows and columns. Below is a table of information that gives the distances in miles between the five largest cities in the United States.

New York Los Angeles Chicago Houston Phoenix

New York

0

2,791

791

1,632

2,457

Los Angeles

2,791

0

2,015

1,546

373

Chicago

791

2,015

0

1,801

1,181

Houston

1,632

1,546

1,801

0

1,176

Phoenix

2,457

373

1,181

1,176

0

The position of each number in the table is a fundamental part of its usefulness. We know that the distance from Chicago to Houston is 1,801 miles because that number is in the Chicago row and the Houston column. A two-dimensional array shares almost all of its properties with a one-dimensional array. It’s still a homogeneous, static data structure with random access. If the example above were made into a Java array, the numbers themselves would be the elements of the array. The names of the cities would need to be stored separately, perhaps in an array of type String.

There’s no reason to confine the idea of a two-dimensional list to a table of values. Many games are played on a two-dimensional grid. One of the most famous such games is chess. As with so many other things in computer science, we must come up with an abstraction that mirrors reality and allows us to store the information inside of a computer. For chess, we’ll need an 8 × 8 two-dimensional array. We can represent each piece in the board with a char, using the encoding given below.

Piece Encoding

Pawn

'P'

Knight

'N'

Bishop

'B'

Rook

'R'

Queen

'Q'

King

'K'

Using uppercase characters for black pieces and lowercase characters for white pieces, we could represent a game of chess after a classic king’s pawn open by white as shown.

0 1 2 3 4 5 6 7

0

'R'
'N'
'B'
'Q'
'K'
'B'
'N'
'R'

1

'P'
'P'
'P'
'P'
'P'
'P'
'P'
'P'

2

3

4

5

'p'

6

'p'
'p'
'p'
'p'
'p'
'p'
'p'

7

'r'
'n'
'b'
'q'
'k'
'b'
'n'
'r'

Observe that, just as with one-dimensional arrays, the indexes for rows and columns in two-dimensional arrays also use zero-based counting.

After the step from one-dimensional arrays to two-dimensional arrays, it’s natural to wonder if there can be arrays of even higher dimension. We can visualize a two-dimensional array as a table, but a three-dimensional array is harder to visualize. Nevertheless, there are uses for three-dimensional arrays.

Consider a professor who’s taking a survey of students in her course. She wants to know how many students there are in each of three categories: gender, class level, and major. If she treats each of these as a dimension and assigns an index to each possible value, she could store the results in a three-dimensional array. For gender she could pick male = 0 and female = 1. For class level she could pick freshman = 0, sophomore = 1, junior = 2, senior = 3, and other = 4. Assuming it’s a computer science class, for major she could pick computer science = 0, math = 1, other science = 2, engineering = 3, and humanities = 4. Using this system she could compactly store the number of students in any combination of categories she was interested in. For example, the total number of female sophomore engineering students would be stored in the cell with gender index 1, class level index 1, and major index 3.

Three dimensions is usually the practical limit when programming in Java. If you find an especially good reason to use four or higher dimensions, feel free to do so, but it should happen infrequently. The Java language has no set limit on array dimensions, but most virtual machines have the absurdly high limitation of 255 different dimensions.

6.7. Syntax: Advanced arrays in Java

Now that we’ve discussed the value of storing data in multidimensional lists, we’ll describe the Java language features that allow you to do so. The changes needed to go from one-dimensional arrays to two-dimensional and higher arrays are simple. First, we’ll describe how to declare, instantiate, and index into two-dimensional arrays. Then, we’ll discuss some of the ways in which arrays (both one-dimensional and higher) are different from primitive data types. Next, we’ll explain how it’s possible to make two-dimensional arrays in Java where the rows are not all the same length. Finally, we’ll cover some of the most common mistakes programmers make with arrays.

6.7.1. Multidimensional arrays

When declaring a two-dimensional array, the main difference from a one-dimensional array is an extra pair of brackets. If we wish to declare a two-dimensional array of type int in which we could store values like the table of distances above, we would do so as follows.

int[][] distances;

As with one-dimensional arrays, it’s legal to put the brackets on the other side of the variable identifier or, even more bizarrely, have a pair on each side.

Once the array is declared, it must still be instantiated using the new keyword before it can be used. This time we will use two pairs of brackets, with the number in the first pair specifying the number of rows and the number in the second pair specifying the number of columns.

distances = new int[5][5];

After the instantiation, we will have 5 rows and 5 columns, giving a total of 25 locations where int values can be stored. Indexing these locations is done by specifying row and column values in the brackets. So, to fill up the table with the distances between cities given above we can use the following tedious code.

// New York
distances[0][1] = 2791;
distances[0][2] = 791;
distances[0][3] = 1632;
distances[0][4] = 2457;
// Los Angeles
distances[1][0] = 2791;
distances[1][2] = 2015;
distances[1][3] = 1546;
distances[1][4] = 373;
// Chicago
distances[2][0] = 791;
distances[2][1] = 2015;
distances[2][3] = 1801;
distances[1][4] = 1181;
// Houston
distances[3][0] = 1632;
distances[3][1] = 1546;
distances[3][2] = 1801;
distances[3][4] = 1176;
// Phoenix
distances[4][0] = 2457;
distances[4][1] = 373;
distances[4][2] = 1181;
distances[4][3] = 1176;

You’ll notice that we did not specify values for distances[0][0], distances[1][1], distances[2][2], distances[3][3], or distances[4][4], since each of these already has the default value of 0.

Much more often, multidimensional array manipulation will use nested for loops. For example, we could create an array with 3 rows and 4 columns, and then assign values to those locations such that they were numbered increasing across each row.

int[][] values = new int[3][4];
int number = 1;
for(int i = 0; i < values.length; i++)
    for(int j = 0; j < values[i].length; j++) {
        values[i][j] = number;
        number++;
    }

This code would result in an array filled up like the following table.

1

2

3

4

5

6

7

8

9

10

11

12

The bounds for the outer for loop in this example uses values.length, giving the total number of rows. Then, the inner for loops uses values[i].length, which is the length (number of columns) of the current row. In this case, all the rows of the array have the same number of columns, but this is not always true, as we’ll discuss later.

6.7.2. Reference types

All array variables are reference type variables, not simple values like most of the types we have discussed so far. A reference variable is a name for an object. You might recall that we described the difference between reference types and primitive types in Section 3.2, but the only reference type we’ve considered in detail is String.

More than one reference variable can point at the same object. When one object has more than one name, this is called aliasing. The String type is immutable, meaning that an object of type String cannot change its contents. Arrays, however, are mutable, which means that aliasing can cause unexpected results. Here is a simple example with one-dimensional array aliasing.

int[] array1 = new int[10];
for(int i = 0; i < array1.length; i++)
    array1[i] = i;
int[] array2 = array1;
array2[3] = 17;
System.out.println(array1[3]);

Surprisingly, the value printed out will be 17. The variables array1 and array2 are references to the same fundamental array. Unlike primitive values, the complete contents of array1 are not copied to array2. Only one array exists because only one array has been created by the new keyword. When index 3 of array2 is updated, index 3 of array1 changes as well, because the two variables are simply two names for the same array.

array3
Figure 6.4 Two array references pointing to a single array object.

Sometimes this reference feature of Java allows us to write code that is confusing or has unexpected consequences. However, the benefit is that we can assign one array to another without incurring the expense of copying the entire array. If you create an array with 1,000,000 elements, copying that array several times could get expensive in terms of program running time.

The best rule of thumb for understanding reference types is that there is only one actual object for every call to new. The primary exception to this rule is that uses of new can be hidden from the user when they’re in method calls.

String greeting = new String("Hello");
String pronoun = greeting.substring(0,2);

At the end of this code, the reference pronoun will point to an object containing the String "He". The substring() method invokes new internally, generating a new String object completely separate from the String referenced by greeting. This code may look unusual because we’re explicitly using new to make a String object containing "Hello". The String class is different from every other class because it can be instantiated without using the new keyword. The line String greeting = "Hello"; implicitly calls new to create an object containing the String "Hello" and functions nearly the same as the similar line above.

6.7.3. Ragged arrays

We’re ashamed to say that we’ve lied to you. In Java, there’s no such thing as a true multidimensional array! Instead, the examples of two-dimensional and three-dimensional arrays we’ve given above are actually arrays of arrays (of arrays). Thinking about multidimensional arrays in this way can give the programmer more flexibility.

If we return to the definition of the two-dimensional array with 3 rows and 4 columns, we can instantiate each row separately instead of as a block.

int[][] values = new int[3][];
int number = 1;
for(int i = 0; i < values.length; i++) {
    values[i] = new int[4];
    for(int j = 0; j < values[i].length; j++) {
        values[i][j] = number;
            number++;
    }
}

This code is functionally equivalent to the earlier code that instantiated all 12 locations at once. The same could be done with a three-dimensional array or higher. We can specify the length of each row independently, and, more bizarrely, we can give each row a different length. A multidimensional array whose rows have different lengths is called a ragged array.

A ragged array is usually unnecessary. The main reason to use a ragged array is to save space, when you have tabular data in which the lengths of each row vary a great deal. If the lengths of the rows vary only a little, it’s probably not worth the extra hassle. However, if some rows have 10 elements and others have 1,000,000, the space saved can be significant.

We can apply the idea of ragged arrays to the table of distances between cities. If you examine this table, you’ll notice that about half the data in it is repeated, because the distance from Chicago to Los Angeles is the same as the distance from Los Angeles to Chicago, and so on. We can store the data in a triangular shape to keep only the unique distance information.

New York Los Angeles Chicago Houston Phoenix

New York

0

Los Angeles

2,791

0

Chicago

791

2,015

0

Houston

1,632

1,546

1,801

0

Phoenix

2,457

373

1,181

1,176

0

We could create this table in code by doing the following.

distances = new int[5][];
// New York
distances[0] = new int[1];
// Los Angeles
distances[1] = new int[2];
distances[1][0] = 2791;
// Chicago
distances[2] = new int[3];
distances[2][0] = 791;
distances[2][1] = 2015;
// Houston
distances[3] = new int[4];
distances[3][0] = 1632;
distances[3][1] = 1546;
distances[3][2] = 1801;
// Phoenix
distances[4] = new int[5];
distances[4][0] = 2457;
distances[4][1] = 373;
distances[4][2] = 1181;
distances[4][3] = 1176;

With this table a user cannot simply type in distances[0][4] and hope to get the distance from New York to Phoenix. Instead, we have to be careful to make sure that the index of the first city is never larger than the index of the second city. If we’re reading in the indexes of the cities from a user, we can write some code to do this check. Let city1 and city2, respectively, contain the indexes of the cities the user wants to use to find the distances between.

if(city1 > city2) {
    int temp = city1;
    city1 = city2;
    city2 = temp;
}
System.out.println("The distance is: " + distances[city1][city2] +
    " miles");

If we wanted to be even cleverer, we could eliminate the zero entries from the table, but then the ragged array would have one fewer row than the original two-dimensional array.

6.7.4. Common pitfalls

Even one-dimensional arrays make many new errors possible. Below we list two of the most common mistakes made with both one-dimensional and multidimensional arrays.

Pitfall: Array out of bounds

The length of an array is determined at run time. Sometimes the number is specified in the source code, but it’s always possible for an array to be instantiated based on user input. The Java compiler doesn’t do any checking to see if you’re in the right range. If your program tries to access an illegal element, it’ll crash with an ArrayIndexOutOfBoundsException.

int[] array = new int[100];
for(int i = 0; i <= array.length; i++)
    array[i] = i;

Here’s a classic example. By iterating through the loop one too many times, the program will try to store 100 into array[100], when the last index of the array is 99. In C and C++, pointer arithmetic allowed a negative index to be valid for an array in some cases. In Java, a negative index will always throw an ArrayIndexOutOfBoundsException.

There are other less common causes for going outside of array bounds. Imagine that you’re scanning through a file that has been redirected to input, keeping a count of the occurrences of each letter of the alphabet in the file.

Scanner in = new Scanner(System.in);
int[] counts = new int[26];
String word;
while(in.hasNext()) {
    word = in.next().toLowerCase();
    for(int i = 0; i < word.length(); i++)
        counts[word.charAt(i) - 'a']++;
}

This segment of code does a decent job of counting the occurrences of each letter. The while loop continues to execute as long as there is another String worth of data to read in the file. The inner for loop iterates through each char in the String and increments the appropriate element of the counts array. By subtracting the value 'a', we normalize the char values 'a' through 'z' to 0 through 25. However, if there’s any punctuation in the file, simply subtracting 'a' will not work. The Unicode value of '.', for example, is 46. The Unicode value of 'a' is 97. Subtracting 97 from 46 will make this code try to increment index -51 of the array. An additional check should be put into this code to make sure that the char value being examined is a letter.

Pitfall: Uninitialized reference arrays

Another problem only comes up with arrays of reference types. Whenever the elements of an array are primitive data types, memory for that type is allocated. Whenever the elements of the array are reference types, only references to objects, initialized to null, are allocated. Because it’s an array of primitive values, the following code works fine.

int[] primitives = new int[100];
primitives[67]++;

The following code, however, will cause a NullPointerException.

String[] references = new String[100];
int x = references[67].length();

Arrays of reference types must initialize each element before using it. The NullPointerException could be avoided as follows.

String[] references = new String[100];
for(int i = 0; i < references.length; i++)
    references[i] = new String();
int x = references[67].length();

In this case, there would be no error, although references[67].length() would still be 0, and that’s probably not what the programmer intended.

A similar error can happen with multidimensional arrays.

int[][] table = new int[10][];
for(int i = 0; i < table.length; i++)
    table[i][i] = i;

Because an array is itself a reference type, the table array contains 10 references to null for each of its 10 rows. Unless those rows are instantiated, the JVM will again throw a NullPointerException when attempting to access an int value in the table. This error confuses many beginner programmers because no reference types appear to be involved.

6.8. Examples: Two-dimensional arrays

Below we give some examples where two-dimensional arrays can be helpful. We start with a simple calendar example, move on to matrix and vector multiplication useful in math, and finish with a game.

Example 6.5 Calendar

We’re going to create a calendar that can be printed to the terminal to show which day of the week each day lands on. Our program will prompt the user for the day of the week the month starts on and for the total number of days in the month. Our program will print out labels for the seven days of the week, followed by numbering starting at the appropriate place, and wrapping such that each numbered day of the month falls under the appropriate day of the week.

Program 6.3 Prints a calendar for a given month, formatted week by week.
import java.util.*;

public class Calendar {
    public static void main(String[] args) {
        String[][] squares = new String[7][7]; (1)
        squares[0][0] = "Sun"; (2)
        squares[0][1] = "Mon";
        squares[0][2] = "Tue";
        squares[0][3] = "Wed";
        squares[0][4] = "Thu";
        squares[0][5] = "Fri";
        squares[0][6] = "Sat";
        for(int i = 1; i < squares.length; i++) (3)
            for(int j = 0; j < squares[i].length; j++)
                squares[i][j] = " ";        
        Scanner in = new Scanner(System.in);
        System.out.print("Which day does your month start on? (0 - 6) ");
        int start = in.nextInt(); //read starting day (4)
        System.out.print("How many days does your month have? (28 - 31) ");
        int days = in.nextInt();  //read days in month (5)
        int day = 1;
        int row = 1;
        int column = start; 
        while(day <= days) { //fill calendar (6)
            squares[row][column] = "" + day;
            day++;
            column++;
            if(column >= squares[row].length) {
                column = 0;
                row++;
            }
        }
        for(int i = 0; i < squares.length; i++) { (7)
            for(int j = 0; j < squares[i].length; j++)
                System.out.print(squares[i][j] + "\t");
            System.out.println();
        }
    }
}
1 First, our code creates a 7 × 7 array of type String called squares. The array needs 7 rows so that it can start with a row to label the days and then output up to 6 rows to cover the weeks. (Months with 31 days span parts of 6 different weeks if they start on a Friday or a Saturday.) The number of columns corresponds to the seven days of the week.
2 Next, we initialize the first row of the array to abbreviations for each day of the week.
3 Then, we initialize the rest of the array to be a single space.
4 Our program then reads from the user the day the month starts on.
5 The program also reads from the user for the total number of days in the month.
6 The main work of the program is done by the while loop, which fills each square with a steadily increasing day number for each column, moving on to the next row when a row is filled.
7 Finally, the two nested for loops at the end print out the contents of squares, putting a tab ('\t') between each column and starting a new line for each row.
Example 6.6 Matrix-vector multiplication

Arrays give a natural way to represent vectors and matrices. In 3D graphics and video game design, we can represent a point in 3D space as a vector with three elements: x, y, and z. If we want to rotate the three-dimensional point represented by this vector, we can multiply it by a matrix. For example, to rotate a point around the x-axis by θ degrees, we could use the following matrix.

matrix

Given an m × n matrix A, let Aij be the element in the ith row, jth column. Given a vector v of length n, let vi be the ith element in the vector. To multiply A by v, we use the following equation to find the ith element of the resulting vector v′.

vector

By transforming this equation to Java code, we can write a program that can read in a three-dimensional point and rotate it around the x-axis by the amount specified by the user.

Program 6.4 Uses matrix multiplication to rotate a point in three-dimensional space.
import java.util.*;

public class MatrixRotate {
    public static void main(String[] args) {
        double[] point = new double[3]; (1)
        System.out.println("What point do you want to rotate?");
        Scanner in = new Scanner(System.in);
        System.out.print("x: "); (2)
        point[0] = in.nextDouble();
        System.out.print("y: ");
        point[1] = in.nextDouble();
        System.out.print("z: ");
        point[2] = in.nextDouble();
        System.out.print("What angle around the x-axis? ");
        double theta = Math.toRadians(in.nextDouble()); (3)
        double[][] rotation = new double[3][3];
        rotation[0][0] = 1; (4)
        rotation[1][1] = Math.cos(theta);
        rotation[1][2] = -Math.sin(theta);
        rotation[2][1] = Math.sin(theta);
        rotation[2][2] = Math.cos(theta);
        double[] rotatedPoint = new double[3];
        for(int i = 0; i < rotatedPoint.length; i++) (5)
            for(int j = 0; j < point.length; j++)
                rotatedPoint[i] += rotation[i][j]*point[j];
        System.out.println("Rotated point: [" + rotatedPoint[0] + (6)
            "," + rotatedPoint[1] + "," + rotatedPoint[2] + "]");
    }
}
1 This program begins by declaring a array of type double to hold the vector.
2 Afterward, it reads three values from the user into it.
3 Then, the program reads in the angle of rotation in degrees and converts it to radians.
4 Next, we use the Math class to calculate the values in the rotation matrix. Note that we do not change the values that need to be zero.
5 We use a for loop to perform the matrix-vector multiplication. Again, the summing done by our calculations uses the fact that all elements of rotatedPoint are initialized to 0.0.
6 Finally, we print out the answer.
Example 6.7 Tic-Tac-Toe

Almost every child knows the game of tic-tac-toe, also known as noughts and crosses. Its playing area is a 3 × 3 grid. Players take turns placing Xs and Os, trying to get three in a row. Strategically, it’s not the most interesting game since two players who make no mistakes will always tie. Still, we present a program that allows two human players to play the game because the manipulations of a two-dimensional array in the program are similar to those for more complicated games such as Connect Four, checkers, chess, or Go. Our program will catch any attempt to play on a location that has already been played and will determine the winner, if there is one.

Games often give rise to complex programs, since rules that are intuitively obvious to humans may be difficult to state explicitly in Java. Our program begins by setting up quite a few variables and objects.

import java.util.*;

public class TicTacToe {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);  (1)
        char[][] board = new char[3][3];      (2)
        for(int i = 0; i < board.length; i++) (3)
            for(int j = 0; j < board[0].length; j++)
                board[i][j] = ' ';
        boolean turn = true;        (4)
        boolean gameOver = false;
        int row, column, moves = 0; (5)
        char shape;
1 First, we create a Scanner to read in data.
2 Then, we declare and instantiate our 3 × 3 playing board as a two-dimensional array of type char.
3 We want any unplayed space on the grid to be the char for a space, so we fill the array with ' '.
4 Next, we declare a boolean value to keep track of whose turn it is and another to keep track of whether the game is over.
5 Finally, we declare variables to hold the row, the column, the number of moves that have been made so far and the current shape ('X' or 'O').

The core of the game is a while loop that runs until gameOver becomes true. The first line of the body of this loop is an obscure Java shortcut often referred to as the ternary operator. This line is really shorthand for the following.

if(turn)
    shape = 'X';
else
    shape = '0';

The ternary operator works with a condition followed by a question mark and then two values separated by a colon. If the condition is true, the first value is assigned, otherwise the second value is assigned. It’s perfect for situations like this where one value is needed when turn is true and another is needed when turn is false. The ternary operator is a useful trick, but it shouldn’t be overused.

        while(!gameOver) {
            shape = turn ? 'X' : 'O';
            System.out.print(shape + "'s turn.  Enter row (0-2): "); (1)
            row = in.nextInt();
            System.out.print("Enter column (0-2): ");
            column = in.nextInt();
            if(board[row][column] != ' ')   (2)
                System.out.println("Illegal move");
            else {  
                board[row][column] = shape; (3)
                moves++;            
                turn = !turn;
                // Print board		(4)
                System.out.println(board[0][0] + "|" 
                         + board[0][1] + "|" + board[0][2]);
                System.out.println("-----");
                System.out.println(board[1][0] + "|" 
                         + board[1][1] + "|" + board[1][2]);
                System.out.println("-----");
                System.out.println(board[2][0] + "|" 
                         + board[2][1] + "|" + board[2][2] + "\n");             
1 After assigning the appropriate value to shape, our code reads in the row and column values for the current player’s next move.
2 If the row and column selected correspond to a spot that’s already been taken, the program gives an error message.
3 Otherwise, the program sets board[row][column] to the appropriate symbol, increments moves, and changes the value of turn.
4 Then, it prints out the board.

Our program doesn’t do any bounds checking on row and column. If a user tries to place a move at row 5 column 3, our program will try to do so and crash. Additional clauses in the if statement could be used to add bounds checking.

Perhaps the trickiest part of our tic-tac-toe program is checking for a win.

                // Check rows			(1)
                for(int i = 0; i < board.length; i++)
                    if(board[i][0] == shape && board[i][1] == shape
                        && board[i][2] == shape)
                        gameOver = true;
                // Check column			(2)
                for(int i = 0; i < board[0].length; i++)
                    if(board[0][i] == shape && board[1][i] == shape
                        && board[2][i] == shape)
                        gameOver = true;
                // Check diagonals		(3)
                if(board[0][0] == shape && board[1][1] == shape
                    && board[2][2] == shape)
                    gameOver = true;
                if(board[0][2] == shape && board[1][1] == shape
                    && board[2][0] == shape)
                    gameOver = true;            
                if(gameOver)      		(4)
                    System.out.println(shape + " wins!");
                else if(moves == 9){	(5)
                    gameOver = true;            
                    System.out.println("Tie game!");        
                }               
            }
        }
    }
}
1 First we check each row to see if it contains three in a row.
2 Then, we check each column.
3 Finally, we check the two diagonals.
4 If any of those checks ended the game, we announce a winner.
5 Otherwise, if the number of moves has reached 9 with no winner, it must be a tie game.

In a larger game (such as Connect Four), we would want to find better ways to automate checking rows, columns, and diagonals. For example, we wouldn’t want to check the entirety of a larger board each move. Instead, we could focus only on the rows, columns, and diagonals affected by the last move.

6.9. Advanced: Special array tools in Java

Arrays are fundamental data structures in many programming languages. There are often special syntactical tools or libraries designed to make them easier to use. In this section, we explore two advanced tools, the enhanced for loop and the Arrays utility class.

6.9.1. Enhanced for loops

In Chapter 5 we described three loops: while loops, for loops, and do-while loops. Although these are the only three loops in Java, there’s a special form of the for loop designed for use with arrays (and some other data structures). This construct is often called the enhanced for loop.

An enhanced for loop does not have the three-part header of a regular for loop. Instead, it’s designed to iterate over the contents of an array or other list. Inside its parentheses is a declaration of a variable with the same type as the elements of the array, then a colon (:), then the name of the array. Consider the following example of an enhanced for loop used to sum the values of an array of int values called array. As with all loops in Java, braces are optional if there’s only one executable statement in the loop.

int sum = 0;
for(int value : array)
    sum += value;

This code functions in exactly the same way as the traditional for loop we would use to solve the same problem.

int sum = 0;
for(int i = 0; i < array.length; i++)
    sum += array[i];

The advantage of the enhanced for loop is that it’s shorter and clearer. There’s also no worry about being off by one with your indexes. The enhanced for loop iterates over every element in the array, no indexes needed!

Enhanced for loops can be nested or used inside of other loops. Consider the following nested enhanced for loops that print out all possible chess pieces, in both black and white colors.

String[] colors = {"Black", "White"};
String[] pieces = {"King", "Queen", "Rook", "Bishop", "Knight", "Pawn"};
for(String color : colors)
    for(String piece : pieces)
        System.out.println(color + " " + piece);

Enhanced for loops do have a few drawbacks. For one, they’re designed for iterating through an entire array. It’s ugly to try to make them stop early, and it’s impossible to make them go back to previous values. They’re also only designed for read access, not write access. The variable in the header of the enhanced for loop takes on each value in the array in turn, but assigning values to that variable has no effect on the underlying array. Consider the following for loop that assigns 5 to every value in array.

for(int i = 0; i < array.length; i++)
    array[i] = 5;

This kind of assignment is impossible in an enhanced for loop. The “equivalent” enhanced for loop does nothing. It assigns 5 to the local variable value but never changes the contents of array.

for(int value : array)
    value = 5;

While enhanced for loops are great for arrays, they can also be used for any data structure that implements the Iterable interface. We discuss interfaces in Chapter 10 and dynamic data structures in Chapter 18 and Chapter 19.

6.9.2. The Arrays class

The designers of the Java API knew that arrays were important and added a special Arrays class to manipulate them.

This class has a number of static methods that can be used to search for values in arrays, make copies of arrays, copy selected ranges of arrays, test arrays for equality, fill arrays with specific values, sort arrays, convert an entire array into a String representation, and more. The signatures of the methods below are given for double arrays, but most methods are overloaded to work with all primitive types and reference types.

Method Purpose
binarySearch(double[] array, double value)

Returns index of value inside array or a negative number if it cannot be found. Adding 1 to the negative number and then negating it will give the index where the value would have been. Note: array must be sorted for this method to work.

copyOf(double[] array, int length)

Returns a copy of array with length length, either truncated or padded if it doesn’t match the length of array.

copyOfRange(double[] array, int from, int to)

Returns a copy of array from the range starting at from and going up to but not including to.

equals(double[] array1, double[] array2)

Returns true if array1 and array2 have the same number of elements, each pair of which is equal.

fill(double[] array, double value)

Fills array with copies of value.

sort(double[] array)

Sorts array using natural ordering. This method can fail for Object arrays in which the objects are not comparable.

toString(double[] a)

Returns a String containing representations of each element separated with commas.

Consult the API for more information. Even though tasks like fill() are simple, it’s worth using the method from Arrays instead of writing your own. The methods in the Java API have often been tuned for speed and use special instructions that are not accessible to regular Java programmers.

6.10. Solution: Game of Life

Here we present our solution to the Conway’s Game of Life simulation. Our program is designed to run the simulation with 24 rows and 80 columns, although it would be easy to change those dimensions.

public class Life {
    public static void main(String[] args) {
        final int ROWS = 24;        (1)
        final int COLUMNS = 80;     
        final int GENERATIONS = 500;
        boolean[][] board = new boolean[ROWS][COLUMNS]; (2)
        boolean[][] temp = new boolean[ROWS][COLUMNS];
        boolean[][] swap;
        for(int row = 0; row < ROWS; row++) (3)
            for(int column = 0; column < COLUMNS; column++)
                board[row][column] = (Math.random() < 0.1);
1 The main() method of our program starts by defining ROWS, COLUMNS, and GENERATIONS as named constants using the final keyword.
2 Next, we create two arrays with ROWS rows and COLUMNS columns. The board array will hold the current generation. The temp array will be used to fill in the next generation. Then, temp will be copied into board, and the process will repeat. The swap variable is just a reference we’ll use to swap board and temp.
3 We randomly fill the board, making 10% of the cells living. Again, you may wish to play with this number to see how the patterns in the simulation are affected.
        for(int generation = 0; generation < GENERATIONS; generation++) { (1)
            for(int row = 0; row < ROWS; row++) (2)
                for(int column = 0; column < COLUMNS; column++) {
                    int total = 0;
                    for(int i = Math.max(row - 1, 0); (3)
						i < Math.min(row + 2, ROWS); i++)
                        for(int j = Math.max(column - 1, 0);
							j < Math.min(column + 2, COLUMNS); j++)
                            if((i != row || j != column) && board[i][j])
                                total++;
                    if(board[row][column])
                        temp[row][column] = (total == 2 || total == 3); (4)
                    else
                        temp[row][column] = (total == 3); (5)
                }
            swap = board; (6)
            board = temp;
            temp = swap;
1 The for loop at the beginning of this segment of code runs once for each generation we simulate.
2 The two nested for loops examine each cell in board.
3 The two for loops nested inside of those loops do the calculations to determine if a cell will be alive or dead in the next generation. These inner loops start one row before the current row and finish one row after the current row. They do the same for columns. The Math.max() and Math.min() methods are used to keep the loops from going out of bounds of the array. When backing up a row or a column, the Math.max() methods make sure that we do not generate an index smaller than 0. When going forward a row or a column, the Math.min() methods make sure that we do not generate an index greater than ROWS - 1 or COLUMNS - 1.
4 After these two innermost for loops have counted the total of living cells around the cell in question, we decide the fate of the cell for the next generation. If the cell’s alive and has exactly 2 or 3 living neighbors, it’ll continue to live.
5 If a cell’s dead, it’ll come to life only if it has exactly 3 living neighbors.
6 After we’ve stored the state of each cell in the next generation into temp, we swap board and temp, using the swap variable. We could have thrown out the old array stored in board instead of swapping it with temp, but then we’d have to create a new array for temp each time, which is less efficient.
            for(int i = 0; i < 100; i++) (1)
                System.out.println();                   
            for(int row = 0; row < ROWS; row++) { (2)
                for(int column = 0; column < COLUMNS; column++)
                    if(board[row][column])
                        System.out.print("*");
                    else
                        System.out.print(" ");
                System.out.println();
            }           
            try { Thread.sleep(500); } (3)
            catch(InterruptedException e) {}
        }
    }
}
1 The first for loop in this segment prints 100 blank lines to clear the screen, as we explained earlier.
2 The two nested for loops print out the state of the current generation, with a * for each living cell and a blank space for each dead one.
3 After the output, the code sleeps for 500 milliseconds to give the effect of an animation. We’ll discuss exceptions in general in Chapter 12 and give more information about the Thread.sleep() method in Chapter 13.

Although 500 milliseconds (half a second) is a long delay for animation, the scrolling effect of the screen makes the pattern of alive and dead cells hard to perceive on a terminal. Some kind of GUI (such as we discuss in Chapter 15) could provide a more pleasing way to visualize the Game of Life, but drawing arbitrary patterns on a GUI presents its own difficulties.

6.11. Concurrency: Arrays

Arrays are critical to concurrent programming in Java. In Chapter 13, we’ll explain how to create independent threads of execution, each of which is tied to a Thread object. If you have a dual, quad, or higher core computer, you might want to use two or four threads to solve a problem, but some programs can use hundreds. How can you keep track of all those Thread objects? In many cases, you’ll hold references to them in an array.

Arrays can also hold large lists of data. It’s common for threaded programs to share a single array which each thread reads and writes to. In this way, memory costs are kept low because there’s only one copy of all the data. In the simplest case, each thread works on some portion of the array without interacting with the rest. Even then, how do you assign parts of the array to the different threads?

We’ll assume that each element of the array needs to be processed in some way. For example, we might want to record whether or not each long in an array is prime or not. If you have k threads and an array of length n where n happens to be a multiple of k, then it’s easy: Each thread gets exactly n/k items to work on. For example, the first thread will work on indexes 0 through n/k - 1, the second thread will work on indexes n/k through 2n/k - 1, and so on, with the last thread working on indexes (k - 1)n/k through n - 1. Not every element in the array will require the same amount of computation, but we often assume that they do because it can be difficult to guess which elements will take more time to process.

What if the number of elements in the array is not a multiple of the number of threads? We still want to assign the work the work as fairly as possible. New programmers are sometimes tempted to use the same arithmetic from the case in which the threads evenly divide the length of the array: Each thread gets ⌊n/k⌋ (using integer division) elements, and we stick the last thread with the leftovers. How bad can that be?

This assignment of work can be very poorly balanced. Consider a case with 10 threads and 28 pieces of data. ⌊28/10⌋ = 2, using integer division. Thus, the first nine threads have 2 units of work to do, but the last thread is stuck with 10! Not only is this unfair; it’s inefficient. The person writing the program probably wants to minimize the total amount of time needed to finish the job. In this case, the time from when the first thread starts to when the last thread finishes is called the task’s makespan. With this division of work, the makespan is 10 units of work.

split1
Figure 6.5 Units of work per thread using a naive strategy.
Example 6.8 Fairly assigning work

A simple way to fix this problem is to look at the value n mod k, the leftovers when you divide n by k. We want to spread those out over the first few threads. We know that any remainder will be smaller than k. If the index of the thread (starting at 0, of course) is less than the remainder, we add an extra element to its work. In this way, 28 units of work spread over 10 threads will give 3 elements to the first 8 threads and 2 elements to the rest. Using this strategy, the makespan becomes 3 units of work, a huge improvement over 10. Finding a way to spreading work across multiple threads to improve efficiency is a form of load balancing, a broad term for dividing work across computing resources.

split2
Figure 6.6 Units of work per thread using a fair strategy.

The program below reads the length of an array and the number of threads from the user and then prints out the amount of work for each one. You should be able to adapt the ideas in it to your own multi-threaded programs in Chapter 13.

Program 6.5 Template for assigning work to threads.
import java.util.*;

public class AssigningWork {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("How long is your array? ");
        int n = in.nextInt();
        System.out.print("How many threads do you have? ");
        int k = in.nextInt();
        int quotient = n / k;
        int remainder = n % k;
        int next = 0;       
        for(int i = 0; i < k; i++) {      
            int work = quotient;
            if(i < remainder)
                work++;
            System.out.println("Thread " + i + " does " + work
                + " units of work, starting at index " + next
                + " and ending at index " + (next + work - 1));
            next += work;
        }
    }
}

6.12. Exercises

Conceptual Problems

  1. Why can’t an array be used to hold an arbitrarily long list of numbers entered by a user? What are strategies that can be used to overcome this problem?

  2. In future chapters, we’ll introduce a data structure called a linked list. A linked list is a homogeneous, dynamic data structure with sequential access (unlike an array, which has random access). You can instantly jump to any place in an array, but you have to step through each element of a linked list to get to the one you want, even if you know its position in the list exactly. On the other hand, inserting values into the beginning of a linked list can be done in one step, while an array would need to be resized and have its contents copied over. List some tasks for which an array would be better than a linked list and vice versa.

  3. Given the following code:

    double[] array1 = new double[50];
    double[] array2 = new double[50];
    for(int i = 0; i < array1.length; i++) {
        array1[i] = i + 1;
        array2[i] = array2.length - i;
    }
    array2 = array1;
    for(int i = 1; i < array1.length; i++)
        array1[i] = array1[i - 1] + array1[i];

    What is the value in array2[array2.length - 1] after this code is executed?

  4. What error will be caused by the following code, and why?

    String[] array = new String[100];
    System.out.println(array[99].charAt(0) +
               " is the first letter of the last String.");
  5. An array of length n in Java typically takes n times the number of bytes for each element plus an additional 16 bytes of overhead. Since an int uses 4 bytes of storage, an array of 100 int elements would take 416 bytes. Consider the following three-dimensional array declaration and allocation.

    int[][][] data = new int[10][5][20];

    How many bytes are allocated for this array? Remember that the 16 byte overhead will occur repeatedly, since Java creates a three-dimensional array as an array of arrays of arrays.

  6. Our original table of city distances allocates 5 · 5 = 25 int elements to store all the distances between the five cities, including repeats. How many int elements are allocated for the triangular, ragged array version of this city distance table? If we used the normal table style, n cities would require n2 int elements. How many elements would the triangular, ragged array version allocate for n cities?

  7. Consider the naive method of dividing an array of length n among k threads that was discussed in Section 6.11: Each thread gets ⌊n/k⌋ (rounded down because of integer division) elements, and the last thread gets any extras. What mathematical expression describes how many extra elements are allocated to the last thread? Can you come up with an example in which the last element gets all the elements? What should have happened in this case using the other, more fair scheme for assigning the data to threads?

Programming Practice

  1. In Example 6.3, our code would not count ship, as an occurrence of ship because of the comma.

    Rewrite the code from Example 6.3 to remove punctuation from the beginning and end of a word. Use a loop that runs as long as the character at the beginning of a word is not a letter, replacing the word with a substring of itself that does not include the first character. Use a second loop to remove non-letters from the end of a word. Be careful to stop if the length of the String becomes 0, as with text that’s entirely composed of non-letters.

  2. In Example 6.3, we wrote a program that counts the occurrences of each word from a list within a text. If the list of words to search within is long, it can take quite some time to search through the entire list. If the list of words were sorted, we could do a trick that would allow us to search much faster. We could play a “high-low” game, searching through the list by checking the middle word in the array. If that word’s too late in the alphabet, repeat the search on the first half of the list. If it’s too early in the alphabet, repeat the search on the second half of the list. By repeatedly dividing the list in half, until you either find the word you’re looking for or narrow your search down to a single incorrect word, you can search much faster. This kind of searching is called binary search and uses around log n comparisons to find an element in a list of n items. In contrast, looking through the list one element at a time takes about n comparisons.

    Rewrite the code from Example 6.3 to use binary search, after applying selection sort from Example 6.2. Although selection sort will take some extra time, you should more than make up the difference with such a fast search. To implement binary search, keep variables for the start, middle, and end of the list. Keep adjusting the three variables until the middle index has the word you are looking for or the start and end variables reach each other. Remember to use the compareTo() method from the String class to compare words.

  3. In Example 6.4, we gave a program that finds the maximum, minimum, mean, standard deviation, and median of a list of values. Another statistic that is sometimes important is the mode, or most commonly occurring element. For example, in the list {1, 2, 3, 3, 3, 5, 6, 6, 10}, the mode is 3. Write a program that can determine the mode of a given list of int values. A list can have multiple modes if more than one element occurs with maximum frequency. For our purposes, we’ll consider any list with multiple modes to have no modes. You may wish to sort the list before starting the process of counting the frequency of each value.

  4. We used the example of tic-tac-toe in Example 6.7 because a more complex game would have taken too much space to solve. The game of Connect Four (or the Captain’s Mistress, as it was originally called) pits two players against each other on a 6 × 7 vertical board. One player uses red checkers while the other uses black. The two players take turns dropping their checkers into columns of the board in which the checkers will drop to the lowest empty row, due to gravity. The goal of the game is to be the first to make four in a row of your color.

    Implement a version of Connect Four for two human players, similar to our version of tic-tac-toe. Many of the ideas are the same, but the details are more complicated. First, a player will only choose a particular column. Your program must then find which row a checker dropped into that column will fall to. Then, the process of counting four in a row is more difficult than the three in a row of tic-tac-toe. You will need more loops to automate the process fully.

  5. Once you’ve mastered the material in Chapter 15, adapt the solution to Conway’s Game of Life from Section 6.10 to display on a graphical user interface. You can use a GridLayout to arrange a large number of JLabel objects in a grid and update their background colors to Color.BLACK and Color.WHITE as needed, using the setBackground() method. (To make these colors visible, you will also need to call the setOpaque() method once on each JLabel with an argument of true.) The Game of Life is much more compelling with a real GUI instead of an improvised command line representation.

Experiments

  1. Creating arrays with longer and longer lengths requires more processor time, since all of those elements must be initialized to some default value. Using an OS time command, determine the amount of time it takes to create an int array of length 10, 10,000, and 10,000,000. In all likelihood, the amount of time that instantiation of the array takes is a small part of the program, and you should see very little difference in those three times. However, time is not the only important resource. When you run a JVM, it has a default heap size that limits the amount of space you can use to create new objects, including arrays. When you exceed this size, your program will crash with an OutOfMemoryError. Experiment with different sizes of arrays until you can estimate the size of your heap within 5MB or so.

    This estimate will be very rough, since the JVM uses other memory in the background. For a more accurate picture, you can use the Runtime.getRuntime().maxMemory() method to determine the maximum JVM memory size and the Runtime.getRuntime().totalMemory() method to determine the total JVM memory being used.

  2. Run the implementation of the word search program using the binary search improvement from Exercise 6.9. Use the OS time command to time the difference between the regular and binary search versions of the program with a long list of words. You may see very little difference on small input, but you can easily find a list of the 1,000 most commonly used words in English on the Internet along with long, copyright free texts from Project Gutenberg. Combining these two into a single input should see a significant increase in speed for the binary search version relative to the regular version.

  3. Generate input files consisting of 1,000, 10,000, and 100,000 random int values. Time our implementation of selection sort from Example 6.2 running on each of these input files and redirecting output to output files. What’s the behavior of the running time as the input length increases by a factor of 10? As a function of n, how many times total does the body of the inner for loop run during selection sort? Does this function closely mirror the increase in running time?

7. Simple Graphical User Interfaces

To a true artist only that face is beautiful which, quite apart from its exterior, shines with the truth within the soul.

— Mahatma Gandhi

7.1. Problem: Codon extractor

Recall from Chapter 5 that we can record DNA as a sequence of nucleotide bases A, C, G, and T. Using this idea, we can represent any sequence of DNA using a String made up of those four letters such as "ATGGAAGTATTTAAATAG".

This particular sequence contains 18 bases and six codons. A codon is a three-base subsequence in DNA. Biologists are interested in dividing DNA into codons because a single codon usually maps to the production of a specific amino acid. Amino acids, in turn, are the building blocks of proteins. The DNA sequence above contains the six codons ATG, GAA, GTA, TTT, AAA, and TAG.

We want to write a program that extracts codons from DNA sequences entered by the user. The program must detect and inform the user of invalid DNA sequences (those containing letters other than the four bases). If the user enters a DNA sequence whose length is not a multiple of three, the final codon should be written with one or two asterisks (*), representing the missing bases.

With your knowledge of String manipulation and loops, this problem should be easy. However, we want to solve it with a graphical user interface, not with the command line interaction we’ve emphasized in previous chapters. That is, the input step should be done with a window that looks similar to the following.

codoninput

And the corresponding output should look very much like this.

codonoutput

7.2. Concepts: User interaction

Many computer programs communicate with a human user. There are at least two ways in which this communication can happen. One way is to use command line input and output. In this case, a program prompts the user for an input and the user responds through the keyboard, usually completing the response by pressing the <return> or <enter> key. Another way to communicate is to use a graphical user interface or GUI. (Some people pronounce “GUI” to sound like “gooey,” but others say “G-U-I.”) In this case, the program displays a window consisting of one or more widgets, such as a button labeled “OK” or a text box in which the user can type some text. Widgets (also known as controls) can include buttons, labels, text areas, check boxes, menus, and many other pre-defined objects for user interaction. While the program waits for the user or does something in the background, the user has the option of using a combination of the keyboard and the mouse to respond to the program. While command line interfaces were dominant until the mid-70s, GUIs have become the prime mode of communication between a program and a human user. This chapter focuses on the design of simple GUIs using a few built-in Java classes. Chapter 15 introduces more advanced tools for constructing complex GUIs.

Example 7.1 User interaction

Figure 7.1(a) shows a Java application interacting with a user through a command line interface. The application asks the user for a temperature value in degrees Fahrenheit, converts it to the equivalent Celsius, and displays it. Figure 7.1(b) shows a similar application interacting with the user through a GUI. In this case, the application creates a window with six widgets (two labels, two text boxes, and two buttons). The user enters a temperature value in the text box below either the Celsius label or the Fahrenheit label and presses the appropriate Convert button. Then, the application displays the equivalent temperature in the other text box.

applicationInterface
Figure 7.1 User interaction with a Java application (a) through a command line interface and (b) through a graphical user interface.

We describe the GUIs we introduce in this chapter as simple because several aspects of GUI creation are hidden by the methods we use. For example, these GUIs do not require the programmer to handle the details of events such as a user pressing an “OK” button or typing text into a text box and pressing the <enter> key. These events will be handled automatically by existing libraries. Chapter 15 discusses the creation of more complex GUIs that require the programmer to program event handling explicitly.

7.3. Syntax: Dialogs and the JOptionPane class

JOptionPane is a utility class for creating GUIs consisting of a single dialog. It offers a variety of ways to create useful dialogs quickly and easily and is part of the larger Java Swing GUI library. In this chapter, we’ll show you how to use the static methods and constants in JOptionPane to construct useful dialogs. Specifically, you’ll learn how to construct the following four types of dialogs.

Information

An information, or message, dialog displays a message to the user. Static method showMessageDialog() creates such a dialog. See Figure 7.2 for an example of a message dialog.

Confirm

A confirm dialog asks a user to confirm a statement. Static method
showConfirmDialog() creates such a dialog. This dialog may return user input as YES_OPTION, NO_OPTION, OK_OPTION, or CANCEL_OPTION. See Figure 7.4 for an example of a Yes-No dialog.

Option

An option dialog asks the user to select one from an arbitrary set of options. Static method showOptionDialog() creates such a dialog. See Figure 7.5 for an example.

Input

An input dialog is useful for obtaining data provided by the user. Static method showInputDialog() creates such a dialog. The user can input a String that might represent a number, a name, or any arbitrary text. See [inputDialogFigure] for an example.

The JOptionPane class can be used to create both modal and non-modal dialogs. A modal dialog is one that forces the user to interact with the dialog before the program can continue. Thus, the dialog is dismissed and the program execution resumes only after the user has responded. Modal dialogs are useful in situations where user input is required for the program to continue.

A non-modal dialog is one that’s displayed on the screen and doesn’t require the user to interact with it for the underlying program to proceed. It’s easy to create a modal dialog using the static methods in the JOptionPane class mentioned earlier. Creation of non-modal dialogs requires a bit more effort and is not covered in this chapter. In the remainder of this chapter we show how to use JOptionPane to create various types of modal dialogs.

7.3.1. Generating an information dialog

Programs often need to generate a message for the user and request a response. The message might be a short piece of information, and the only response might be “OK.” Alternatively, the message might be more complex and require a more thoughtful response. In this section, we show how the Java utility class JOptionPane can generate a simple dialog whose sole purpose is to inform the user that a task has been completed.

Example 7.2 Simple dialog

Program 7.1 creates a dialog to inform the user that the task it was assigned to perform is now complete. Figure 7.2 shows the dialog generated by this program.

Program 7.1 Generates a simple dialog.
import javax.swing.*; (1)

public class SimpleDialog {    
    public static void main(String [] args) {     
        JOptionPane.showMessageDialog(null, (2)
            "Task completed. Click OK to exit.",         
            "Simple Dialog", JOptionPane.INFORMATION_MESSAGE);
        System.out.println("Done."); (3)
    }
}
1 We import classes used in this program. The swing package contains a number of classes needed to create a GUI, and JOptionPane is one such class.
2 These lines use a static method to create a modal dialog. JOptionPane is a utility class, and showMessageDialog() is a static method in this class. This method, along with the other three JOptionPane methods we discuss in this chapter, is a factory method, meaning that it creates a new object (in this case some kind of dialog object) on the fly with specific attributes. In this example, the program informs the user that a task has been completed. The method has the following four parameters.
Component

The parent component in which the dialog is displayed. We use null in this example, which causes a default frame to be used, centering the dialog in the screen.

Message

The message to be displayed. In this example, we have
"Task completed. Click OK to exit."

Title

The title of the dialog. In this example, it is "A Simple Dialog".

Message Type

The type of the message to be displayed. In this example, we use the constant INFORMATION_MESSAGE.

Icon

The icon to be displayed in the dialog. If you have an object of type Icon, you can use it to customize your dialog. The showMessageDialog() is an overloaded method that can take several different sets of parameters. In this example, we used a version of the method that does not specify an icon.

3 We display a message on the terminal which isn’t needed in this program but illustrates an interesting point. When you run SimpleDialog, you’ll notice that the "Done." message displays on the terminal only after you’ve clicked the “OK” button. This modal behavior blocks execution of the thread that generated it until the button is pressed.

In Figure 7.2 the dialog titled “Simple Dialog” includes an icon, a message, and a button labeled “OK.” This dialog is actually a frame, which is what windows are called in Java. We’ll discuss frames in greater detail in Section 15.3.

simpleMessageFigure
Figure 7.2 A simple dialog generated using JOptionPane.

The appearance of the dialog may be different on your computer. Even though Java is platform independent, GUIs are customized based on the OS you’re running. Each OS has a default look and feel (L & F) manager that specifies how widgets look and behave in your program. You can change the L & F manager, but not all managers are available on all operating systems.

In the previous example, we displayed a message of type INFORMATION_MESSAGE. There are additional message types that could be used.

  • ERROR_MESSAGE

  • PLAIN_MESSAGE

  • QUESTION_MESSAGE

  • WARNING_MESSAGE

When used as parameters in showMessageDialog(), the constants above cause different default icons to be displayed in the dialog box. Figure 7.3 shows dialogs generated by showMessageDialog() when using JOptionPane.ERROR_MESSAGE, (left) and JOptionPane.WARNING_MESSAGE (right). Note the difference in the icons displayed toward the top left of the two dialogs.

iconsinMessageDialogsFigure
Figure 7.3 The left dialog uses JOptionPane.ERROR_MESSAGE, and the right uses JOptionPane.WARNING_MESSAGE. The only difference is the icon displayed.

7.3.2. Generating a Yes-No confirm dialog

There are situations when a program needs to obtain a binary answer from the user, a “yes” or a “no.” The next example shows how to generate such a dialog and how to get the user’s response.

Example 7.3 Yes-No dialog

Consider a program that checks whether a student understands the difference between odd and even integers. The program generates a random integer x, presents it to the user, and asks the question, “Is x an odd integer?” The answer given by the user is checked for correctness, and the user is informed accordingly. Program 7.2 shows how to use the JOptionPane class to generate a dialog for such an interaction.

Program 7.2 Tests knowledge of odd and even integers with a Yes-No dialog.
import javax.swing.*;
import java.util.*;

public class OddEvenTest {
    public static void main(String [] args) { 
        String title = "Odd Even Test";
        Random random = new Random(); (1)
        int x = random.nextInt(10); 
        String question = "Is " + x + " an odd integer?";
        int response = JOptionPane.showConfirmDialog(null, (2)
            question, title, JOptionPane.YES_NO_OPTION);
        String message;     
        // Response is YES_OPTION for yes, NO_OPTION for no
        if((response == JOptionPane.YES_OPTION && x % 2 != 0) ||
            (response == JOptionPane.NO_OPTION && x % 2 == 0))
            message = "You're right!";
        else
            message = "Sorry, that's incorrect.";
        JOptionPane.showMessageDialog(null, message, title, (3)
            JOptionPane.INFORMATION_MESSAGE);
    }   
}
1 We declare a random number generator named random and then use it to generate a random number from 0 to 9.
2 We present the number to the user. Note the use of JOptionPane.YES_NO_OPTION as the last parameter in the showConfirmDialog(). The generated dialog is shown in Figure 7.4(a). The call to showConfirmDialog() returns the JOptionPane.YES_OPTION or the JOptionPane.NO_OPTION value depending on whether the user clicked the “Yes” or “No” button.
3 A second dialog is shown with a message dependent on whether the user gives the correct answer. The two different versions of this dialog are shown in Figure 7.4(b) and (c).
yes noDialogFigure
Figure 7.4 (a) A Yes-No dialog generated using JOptionPane. (b) Dialog in response to correct answer. (c) Dialog in response to incorrect answer.

Because we used YES_NO_OPTION, the dialog in Example 7.3 automatically generates two buttons labeled “Yes” and “No.” Dialogs can also use the YES_NO_CANCEL_OPTION to generate a dialog with “Yes,” “No,” and “Cancel” options. The return value from showConfirmDialog() is CANCEL_OPTION if the user presses the “Cancel” button.

7.3.3. Generating a dialog with a list of options

The JOptionPane class can also be used to generate an arbitrary set of options as shown in the next example.

Example 7.4 Capital dialog

Consider a program that asks the user to select the correct capital of a country from a list of capitals. It shows three options and asks the user to select one from among the three. It then checks the user response for correctness and displays a suitable message.

Program 7.3 Generates a dialog with programmer-defined options.
import javax.swing.*;

public class CapitalQuiz {    
    public static void main(String[] args) {     
        String title = "Capital Quiz";
        String country = "Azerbaijan";
        String[] capitals = {"Bujumbura","Baku", "Moroni"};
        int correct = 1; //Baku is the correct answer        
        String question = "Select the capital of " + country + ".";
        int response = JOptionPane.showOptionDialog(null, (1)
            question, title, JOptionPane.PLAIN_MESSAGE,
            JOptionPane.QUESTION_MESSAGE, null, capitals, null);
        //Response is 0, 1, or 2 for the three options
        String message;     
        if(response == correct) 
            message = "You're right!";
        else
            message = "Sorry, the capital of " + country +
				" is " + capitals[correct] + ".";
        JOptionPane.showMessageDialog(null, message, title, (2)
            JOptionPane.INFORMATION_MESSAGE);        
    }   
}
1 We call the showOptionDialog() method to create a dialog with multiple options. In our case, the options are three names of capitals, and only one of them is correct. Figure 7.5 shows the dialog created.
The showOptionDialog() method creates an options dialog, which is the most complicated (but also the most flexible) of all the dialogs. The array of String values provided as the second to last parameter to showOptionDialog() gives the labels for the buttons.
There are three null values passed into this method. The first one functions like the null used in Program 7.2, specifying that the default frame should be used. The second specifies that the default icon should be used. In the next section, we’ll show how to specify a custom icon. The last parameter indicates the default button, which will have focus when the dialog is created. If the user hits <enter> instead of clicking, the button with focus is the button that will be pressed.
2 As in Program 7.2, a second dialog is shown with a message dependent on whether the user gives the correct answer.
selectCapitalDialogFigure
Figure 7.5 A dialog with programmer-defined options generated by Program 7.3.

7.3.4. Generating a dialog with a custom icon

A custom icon can be included in any dialog. Each of the methods in JOptionPane introduced earlier can take an icon as a parameter. The next example illustrates how to do so.

Example 7.5 Custom icon

Program 7.4 shows how to use showMessageDialog() to generate a message dialog with a custom icon.

Program 7.4 Generates a dialog with a custom icon.
import javax.swing.*;

public class CustomIconDialog{
    public static void main(String [] args){     
        String file = "bat.png";
        String title = "Custom Icon";
        String message = "Some bats eat 3,000 mosquitoes a night.";
        JOptionPane.showMessageDialog(null, message, title,
            JOptionPane.INFORMATION_MESSAGE, new ImageIcon(file)); (1)
    }
}
1 The last parameter creates a new ImageIcon object from the file String ("bat.png" in this case). The resulting dialog appears in Figure 7.6.

Dialogs illustrated in earlier examples can also use an icon parameter to include a custom icon.

customIconDialogFigure
Figure 7.6 Dialog with a custom icon generated by Program 7.4.

Note that the icon shown above will not appear when you run this code unless you have a copy of bat.png in the appropriate directory.

7.3.5. Generating an input dialog

An input dialog can read text data from the user. The showInputDialog() method in the JOptionPane class allows us to create such a dialog. We introduced the showInputDialog() method in Section 2.3, but we give two more examples here to emphasize its similarity to the other JOptionPane factory methods and to show off some of its additional features.

Example 7.6 Input dialog

We want to write a program that asks a question about basic chemistry. Program 7.5 shows how to display a question, obtain an answer from the user, check for the correctness of the answer, and report back to the user.

Program 7.5 Generates a dialog to input data as text.
import javax.swing.*;

public class ChemistryQuizOne {
    public static void main(String [] args) {
        String title = "Atoms in Water";
        String query = "How many atoms are in a molecule of water?";        
        String response = JOptionPane.showInputDialog(null, (1)
            query, title, JOptionPane.QUESTION_MESSAGE);
        int answer = Integer.parseInt(response); (2)
        String message;         
        if(answer == 3) (3)
            message = "You're right!";
        else
            message = "Sorry, that's incorrect.";
        JOptionPane.showMessageDialog(null, message, title, (4)
            JOptionPane.INFORMATION_MESSAGE);
    }   
}
1 We use the showInputDialog() method to generate the dialog shown in Figure 7.7. This method returns a String named response containing the text entered by the user in the dialog box.
2 We convert this String to an int and save it into variable answer.
3 We check this value against the correct answer.
4 The showMessageDialog() method informs the user whether or not the answer is correct.

It’s important to note that the user could type any sequence of characters in the dialog box. Try running Program 7.5 and see what happens when you type “two,” instead of the number “2,” into the dialog box and press the “OK” button. The program will generate an exception indicating that the input String cannot be converted to an integer.

inputDialogTextFigure
Figure 7.7 Dialog to read text input generated by Program 7.5.
Example 7.7 Input dialog with a list

In Example 7.6 the user is required to enter text. To reduce input errors, we can restrict the user to picking from a predefined list. We can create this list by generating an array and supplying it as a parameter to the showInputDialog() method.

Program 7.6 displays a list of chemical elements and asks the user to select the heaviest.

Program 7.6 Generates a dialog to input a choice from a list.
import javax.swing.*;

public class ChemistryQuizTwo {
    public static void main(String [] args) {     
        String title = "Heaviest Element";
        String query = "Which is the heaviest element?";
        String[] elements = {"Iron", "Uranium", "Copernicium", "Nitrogen"};
        String response = (String)JOptionPane.showInputDialog(null, (1)
            query, title, JOptionPane.QUESTION_MESSAGE, null, elements, null);
        String message;
        if(response.equals("Copernicium")) (2)
            message = "You're right!";
        else
            message = "Sorry, correct answer: Copernicium.";               
        JOptionPane.showMessageDialog(null, message, title, (3)
            JOptionPane.INFORMATION_MESSAGE);
    }   
}
1 We pass an array of four String values to the showInputDialog() method. Note that the last parameter to this method is null indicating that no specific item on the list should be selected by default. (In this case, the first item in the list is initially selected.) The generated dialog is shown in Figure 7.8. The four elements are contained in a drop-down list.
Unlike Example 7.6, the return value from showInputDialog() is now of type Object, not of type String. The type of the list required by the method is Object array. You’re allowed to pass a String array to a method that wants an Object array due to inheritance, which is further discussed in Chapter 11 and Chapter 17. The return value is the specific object from the array that was passed in. In our case, it has to be a String, but the compiler isn’t smart enough to figure that out. For this reason, we cast the object to a String before using the equals() method.
2 We check this String for correctness.
3 As before, the showMessageDialog() method informs the user whether or not the answer is correct.
inputDialogListFigure
Figure 7.8 Dialog to read input from a list generated by Program 7.6.

Note that this program will crash if the user clicks the “Cancel” button, since null will be returned and stored into response.

Example 7.8 Input dialog with a long list
inputDialogListManyItemsFigure
Figure 7.9 A dialog requesting user to select one item from a list of 20 data items. Note the use of a scroll down list to display the items.

When the number of elements in the list supplied to the showInputDialog() is 20 or more, a JList object is automatically used to display the items as shown in Figure 7.9.

Other than a longer list, the code in this example is virtually identical to the code in Program 7.6.

7.4. Solution: Codon extractor

Here we give the solution to the codon extractor problem posed at the beginning of the chapter. As we have done throughout this chapter, we start with the import needed for GUIs built on the Swing framework. Next we begin the CodonExtractor class and its main() method. For readability, the solution to this problem is divided into methods that each do a specific task. We hope that the way a method works is intuitively clear to you. If not, the next chapter explains them in detail.

import javax.swing.*;

public class CodonExtractor {
    public static void main(String [] args) {       
        int continueProgram;
        do { (1)
            // Read DNA sequence
            String input = JOptionPane.showInputDialog("Enter a DNA sequence"); (2)
            input = input.toUpperCase(); (3)
            String message = "Do you want to continue?";
            if(isValid(input)) (4)
                displayCodons(input); (5)
            else
                message = "Invalid DNA Sequence.\n" + message;
            continueProgram = JOptionPane.showConfirmDialog((6)
                null, message, "Alert", JOptionPane.YES_NO_OPTION);            
        } while(continueProgram == JOptionPane.YES_OPTION);
        JOptionPane.showMessageDialog(null, "Thanks for using the Codon Extractor!");  
    }
1 The main() method contains a do-while loop that allows the user to enter sequences repeatedly.
2 The showInputDialog() method makes an input dialog and returns the String the user enters.
3 The toUpperCase() method converts the String to uppercase, allowing us to read input in either case.
4 We then call the isValid() method to make sure that the user entered a valid DNA sequence.
5 If it is valid, we use displayCodons() to display the codons in the sequence.
6 Either way, we use a showConfirmDialog() method to creating a confirm dialog, asking the user if he or she wants to continue entering sequences. The loop will continue as long as the return value is JOptionPane.YES_OPTION.
    public static boolean isValid(String dna) {
        String validBases = "ACGT";                
        for(int i = 0; i < dna.length(); i++) {
            char base = dna.charAt(i);
            if(validBases.indexOf(base) == -1)          
                return false; //base not in "ACGT"
        }        
        return true;
    }

The isValid() method checks to see if the DNA contains only the letters representing the four bases. To do this, we use the Java String library cleverly: We loop through the characters in our input, checking to see where they can be found in "ACGT". If the index returned is -1, the character was not found, and the DNA is invalid.

In the displayCodons() method, we display the individual codons to the user.

    public static void displayCodons(String dna) {                
        String message = "";
        // Get as many complete codons as possible
        for(int i = 0; i < dna.length() - 2; i += 3) (1)
            message += "\n" + dna.substring(i, i + 3);
        // 1-2 bases might be left over
        int remaining = dna.length() % 3;        
        if(remaining == 1)
            message += "\n"+ dna.substring(dna.length() - 1, dna.length()) + "**";
        else if(remaining == 2)
            message += "\n"+ dna.substring(dna.length() - 2, dna.length()) + "*";
        message = "DNA length: " + dna.length() + "\n\nCodons: " + message;
        JOptionPane.showMessageDialog(null, message, (2)
            "Codons in DNA", JOptionPane.INFORMATION_MESSAGE);
    }
}
1 We build a large String with newlines separating each codon. To do so, we loop through the input, jumping ahead three characters each time. If the input length is not a multiple of three, we pad with asterisks.
2 We use the showMessageDialog() method to display an information dialog with the list of codons.

7.5. Concurrency: Simple GUIs

Many GUI frameworks (including Swing) are built on a multi-threaded model. Swing uses threads to redraw widgets and listen for user input while the main thread can continue processing other data.

In this chapter, the impact of these threads is minimal because we used only modal dialogs. Every time we called a JOptionPane method, the execution of the program’s main thread had to wait until the method returned. As it turns out, several threads are created when showInputDialog() or any of the others dialog methods are called, but they do not interact with the main thread since it’s been blocked.

The situation is more complicated with a non-modal dialog, which is one of the reasons we did not go into them. In a non-modal dialog, the threads that redraw the dialog and handle its events (like a user clicking on a button) are running at the same time as the thread that created the dialog. Since many threads are running, it’s possible for them to write to the same data at the same time. Doing so can lead to inconsistencies such as the ones we’ll describe in Chapter 14.

The GUIs we’ll create in Chapter 15, however, will be more than dialogs. They will be fully functional windows, known as frames in Java. Like a non-modal dialog, the creation of a frame doesn’t block the thread that created it.

Many applications launch a frame and then end their main thread. If no other threads are created, such a program is comparatively easy to think about. However, complex applications may create multiple frames or launch threads to work on tasks in the background. Another common problem is caused by performing complicated tasks in the event handler for a GUI. If a task takes too long, the GUI can freeze or become unresponsive, as you’ve probably experienced. The fact that this problem happens so frequently even in the latest operating systems should hint at the difficulty of managing GUI threads.

When we describe how to create fully featured GUIs in Chapter 15, we’ll also give some techniques to help with avoiding unresponsive GUIs in a multi-threaded environment.

7.6. Summary

In this chapter we’ve introduced a way to create simple GUIs. These GUIs are created using various methods available in the JOptionPane class. While the interfaces created this way are limited in scope, they’re often adequate for input and output in short Java programs. Construction of more complex GUIs is the subject of Chapter 15.

7.7. Exercises

Conceptual Problems

  1. In which situations would it be better to use a command-line interface instead of a GUI? When is it better to use a GUI over a command-line interface?

  2. Explain the difference between a modal and a non-modal dialog. Give an example of when you would prefer a modal over a non-modal dialog and another example of when you would prefer a non-modal to a modal dialog.

  3. Give one example each when you would use the five different message type constants in showMessageDialog() method.

  4. In Program 7.2, we could have coded the line checking to see if the user had correctly determined whether the number was odd or even as follows.

    if((response == 0 && x % 2 != 0) ||
       (response == 1 && x % 2 == 0))

    Yet another option is below.

    if(response != x % 2)

    Which of these three implementations is best? Why?

Programming Practice

  1. Modify the program in Example 7.3 such that it tests the user many times whether a randomly generated integer is odd or even. The program should keep a score indicating the number of correct answers. At the end of the test, display the score using a suitable dialog.

  2. Modify the program in Example 7.3 such that it displays a dialog that asks the user “Do you wish to continue?” and offers options “Yes” and “No.” The program should exit the loop when the “No” option is selected and display the score using a suitable dialog.

  3. Rewrite Program 7.2 so that the dialog generated offers the “Yes,” “No,” and “Cancel” options to the user. The program should exit with a message dialog saying “Thank You” when the user selects the “Cancel” option.

  4. Modify Program 7.3 to create and administer a test wherein the user is asked capitals of 10 countries in a sequence. The program must keep a count of the number of correct answers. Inform the user of the score at the end of the test using a suitable dialog.

  5. Modify Program 7.3 so that the button labeled “Baku” has focus when the program begins.

  6. Section 8.5 gives a method called shuffle() that can randomize an array representing a deck of cards. Adapt this code and modify Program 7.3 so that the order of the capitals is randomized. Note that you will need to record which index contains the correct answer.

  7. Re-implement the solution to the college cost calculator problem given in Section 3.5 so that it uses GUIs constructed with JOptionPane for input and output.

  8. Re-implement the solution to the Monty Hall problem given in Section 4.4 so that it uses GUIs constructed with JOptionPane for input and output.

  9. Re-implement the solution to the DNA searching problem given in Section 5.4 so that it uses GUIs constructed with JOptionPane for input and output.

  10. Write a program that creates an input dialog that prompts and reads a file name of an image from the user. Then, create an information dialog that displays the file as a custom icon. In this way, you can construct a simple image viewer.

  11. Use a try-catch block and modify Program 7.5 so that it handles an exception generated when the user enters text that cannot be converted to an integer. In the event such an exception is raised, pop up a message dialog box informing the user to try again and type an integer value. When the user responds by clicking the “OK” button on this message box, the input dialog box should appear once again and offer the user another chance at the answer. Write two versions of the modified program. In one version, your program should give only one chance for input after an incorrect string has been typed. In another version, your program should remain in a loop until the user enters a valid integer. There is, of course, no guarantee that an answer is correct just because it’s a valid integer.

    Note: You should attempt this exercise only if you’re familiar with exceptions in Java. Exceptions are covered in Chapter 12.

8. Methods

Polonius (Aside): Though this be madness, yet there is method in ’t.

— William Shakespeare

8.1. Problem: Three card poker

Gambling has held a fascination for humankind since its invention. As long as there have been mathematicians, they have studied the underlying mechanisms of probability and statistics that drive games of chance. A classic game of both statistics and strategy is poker. The problem we want to solve is programming one of the many variations of poker, called three card poker. Instead of bluffing, a player competes only with the house according to fixed rules without any room for psychology. A player is dealt three cards. If the player’s hand contains a pair or better, he or she wins a payoff greater than or equal to the money bet. If the player does not have a pair or better, he or she loses the money bet. Below is a table giving one possible set of payoffs for each possible hand.

Hand Payoff Hand Payoff

Straight Flush

40

Flush

2

Three of a Kind

30

Pair

1

Straight

6

Nothing

0

If you’re unfamiliar with poker rules or card games in general, here’s a quick explanation of the various hands. A traditional English or American deck of cards is made up of 52 cards, organized into four suits: spades, hearts, diamonds, and clubs. In each suit, there are 13 ranks: two, three, four, five, six, seven, eight, nine, ten, jack, queen, king, and ace.

In three card poker, a straight is when the three cards can be arranged so that their ranks (ignoring suit) are in order. For example, a hand consisting of a Four of Diamonds, a Five of Spades, and a Six of Clubs would constitute a straight. An ace can serve as either the highest rank card (coming after a king) or the lowest rank card (coming before a two) and can help form a straight in either role (but not both at the same time). A flush is when all three cards have the same suit. For example, a Three of Hearts, a Seven of Hearts, and a Jack of Hearts would constitute a flush in three card poker. A straight flush, the highest hand, is made up of cards that form both a straight (by rank ordering) and a flush (by uniformity of suit). A three of a kind occurs when all three cards have the same rank, and a pair occurs when two of the cards have the same rank. When none of these conditions hold, the hand has no special designation in three card poker, and the bet is lost.

Your task is to write a program that serves as a computerized version of the game. You must create a deck of 52 cards, thoroughly randomize them to simulate shuffling, and select the first three cards as a hand. You must then determine the highest designation that applies to the hand of cards and the corresponding winnings (if any).

Any reasonable solution to this problem uses arrays, discussed in the previous chapter. Using only that knowledge, you should be able to create a three card poker game. However, the focus of this chapter is methods, which can be used to break the solution to a problem into logical pieces. By dividing the solution in this way, we’ll be able to solve the three card poker problem in a relatively short, elegant, and easy to read way.

8.2. Concepts: Dividing work into segments

You should notice that the solutions to problems we’ve been working on have become increasingly complex as the book progresses. This progression is partly because we want to solve more interesting problems with more complex solutions but also because we’ve introduced additional Java tools that make solving these harder problems possible.

Surprisingly, the tools we introduce in this chapter do not allow you to solve a problem you couldn’t solve before. Instead, the tools in this chapter and in the next make solving a problem easier and less susceptible to errors. A relatively long, complicated list of Java statements was necessary to solve problems like the statistics program or Conway’s Game of Life. Instead of solving those problems as a single monolithic segment of code, we can break our solutions into units called static methods.

8.2.1. Reasons for static methods

A static method is a short, named segment of code that’s been packaged up so that it can be called from other parts of the program. Whenever the task performed by this method is needed, the execution of the program jumps to the code in the method, does the work it’s supposed to, and then returns to whatever it was doing before.

The reasons for using static methods can be boiled down to three essentials:

Modularity

Very little software is written by individuals. Commercial software can have tens or even hundreds of developers involved in designing, implementing, testing, and maintaining code. When the code’s divided into individual methods, those methods can be written and tested by different people with minimal interference. It’s much harder to work together on one giant block of code.

Readability

Methods are named segments of code. When a method is called, its name is used. If a meaningful name is chosen, the line in the code that calls the method helps document the code by explaining what’s happening. For example, if a method called sort() is used, a reader instantly understands that something’s being sorted. If, instead of a separate sort() method, the code that does sorting was pasted into the same location, a reader might have to devote a few moments to understanding the meaning of those lines of code, particularly without comments.

Reusability

The fact that a method can be called from anywhere in the code makes it reusable. To use the sort() example again, we might have to sort several different arrays in one program. Without methods, we’d have to keep copying and pasting code over and over again. With methods, the total amount of source code can be smaller. In fact, if you find yourself copying and pasting code, it’s often an indication that a method would be useful.

This idea of reusability stretches beyond individual programs. A static method can be called from an entirely different program. If you create a method that’s really useful, you’re free to use it in other programs you write. By doing so, you only have to makes changes in one place if you discover errors or want to increase its functionality.

8.2.2. Parallel to mathematical functions

We’ve been discussing the usefulness of static methods without saying exactly what they are. One way to get insight into methods is by acknowledging their similarity to functions from mathematics. In procedural (non-object oriented) languages like C, the equivalents of static methods are usually referred to as functions. Like functions in mathematics, a static method takes some number of inputs (possibly as few as zero) and usually produces some output.

You’ve already used several static methods from the Math class, such as Math.sqrt(). Consider the following function.

root

This function mirrors the code inside of Math.sqrt(). In math, when you apply f(x) to a specific value, you might write y = f(5). In Java, you might type the following.

double y = Math.sqrt(5);

In the statement f(5), the placeholder variable x takes on the value 5. In the same way, some variable inside of Math.sqrt() takes on the value 5 when the method is called.

It’s important to understand this similarity between methods and mathematical functions because the designers of many programming languages have been directly influenced by the older notation. However, there are many differences between the two concepts as well. Mathematical functions can take more than one input, and so can Java methods. Java methods can also take no input, while the central idea of a mathematical function is to map some input value(s) to some output value. Java methods do not necessarily even have output values. They may just do something.

8.2.3. Control flow

We have discussed selection statements such as if and switch as well as three different looping structures, for, while, and do-while. Each of these Java language features is used for control flow. The selection statements allow your program to choose which code to execute. The loops allow your code to repeatedly execute certain code. Methods also affect control flow. When a method is called, the execution of the program jumps into the method, does all the work it needs to there, and then returns to the code that called it. Recall the very simple example from above.

double y = Math.sqrt(5);

The JVM has allocated a variable of type double and is preparing to assign a value to it. Suddenly, the execution of the JVM jumps into the code inside of the Math.sqrt() method. It takes some non-trivial amount of work to compute the square root of a number. After that computation is finished, the flow of execution returns to the assignment that has been waiting all that time. Just like a mathematical function, we can treat the method call Math.sqrt(5) as if it were “magically” replaced by a double approximating the square root of 5.

8.3. Syntax: Methods

By now, you should have a good feel for the concepts behind calling and even creating static methods and are probably getting impatient to use them. However, we haven’t yet described all the details of Java method syntax. First, we’ll describe how you can create your own static methods, then we’ll discuss the finer points of calling static methods, and finally we’ll explain how class variables can be used from many different methods.

8.3.1. Defining methods

A very simple method that the Math class provides is the Math.max() method. This method selects the larger of the two values provided as input.

int maximum = Math.max(5, 10);

In this case, the value stored into maximum is 10. Despite its simplicity, we demonstrated how useful this method could be in our solution to Conway’s Game of Life from Chapter 6. If we wanted to write this method ourselves, the code would be as follows.

public static int max(int a, int b) {
    if(a >= b)
        return a;
    else
        return b;
}

Even in such a small method, there’s a lot of syntax to worry about. The first line of this method is called the method header. The public keyword in this header is used to denote that any code, even code from a different class, can call this method. We discuss restricting access to methods and variables more in the later part of this chapter. For now, assume that every method is public.

The keyword static indicates that this method is static. Although we have used the term static method many times, we have not yet defined it. A static method is linked to a whole class, not to a specific object of that class–that is, a static method can be called without referencing an object of the class. Again, we discuss the finer points of objects and classes in the next chapter. For now, all methods are static.

The third keyword in the method header is the familiar int, giving the return type of the method. Wherever this method is called, it can be treated like an int value, because that’s what it gives back. In this case, the return type is obvious: The maximum of two int values must also be an int value. Any type can be used as a return value including all the primitive types and any reference or array types. The only limitation is that a method can only return a single item, but since that item can be an array, this limitation is usually not important. It’s also possible for a method to return nothing. In that case, the keyword void is used for the return type.

Next in the method header is the identifier max, which is the name of the method. Any legal identifier that you can use for a variable name is valid for a method name as well. It’s important to pick a name that’s readable and gives a reader a clear idea about what the method does. A common convention is to name a method using a verb phrase, indicating the operation that is being done by that method (e.g., computeTax). Like variable names, the Java standard is to use camel notation, starting with a lowercase letter and capitalizing the first letter of each new word in the name.

After the name of the method is the list of parameters, separated by commas. In this case, each parameter has the int type. You’re free to name your parameters whatever you want, though they should be meaningful. You can have as few as zero parameters, but there’s an upper limit imposed by the JVM, usually 255. The body of the method follows the header of the method, surrounded by braces ({ }). Unlike if statements and loops, the braces for methods are required.

Inside the body of a method, the usual rules for Java control flow apply. Each line is executed one by one unless there are selection statements or loops. Calling methods inside of methods is also allowed. In the max() method, we use an if-else construction to find the larger of a and b. A return statement immediately stops execution of the method, transfers execution back to the calling code, and gives back the value that comes after it. In this case, the value of a is returned if it is equal or larger, and the value of b is returned otherwise. Because a return statement immediately jumps out of a method, we could have written the method with one fewer line of code.

public static int max(int a, int b) {
    if(a >= b)
        return a;
    return b;
}

The only way that the line return b; can be reached is if a had not already been returned.

Beginner programmers sometimes ask what to do if a and b are equal. In our code it’s clear that the value of a will be returned, but it hardly matters: two equal int values are indistinguishable from each other. Some students imagine handling this special case by printing an error message, but that approach is seldom useful. When trying to find the larger of two numbers in programming, it’s usually the value we want, not information about which one is actually larger.

The main() method

If some of this syntax seems eerily familiar, remember that you’ve been coding static methods since your very first Java program. The main() method is just another static method, special only because the JVM chooses to start execution there. Let’s look at the main() method from a standard Hello, World! program.

public static void main(String[] args) {
    System.out.println("Hello, world!");
    return;
}

Just like the max() method, the header for main() starts with public static. Then, the return type for main() is void because the JVM is not expecting to get any answer back. The main() method has a single parameter, an array String values. In this program, we do not use the args parameter, but it is available. For the main() method, this declaration is fine, because main() has to be uniform across all programs. However, when designing your own methods, you should not include unnecessary parameters.

The final executable line in this main() method is a return statement. Because main() has a void return type, the return statement has no value to return. For void methods, a return statement is optional. You can use it to leave a method early if desired. For a value-returning method, execution must reach a return statement with a valid value no matter what the input of the method is. If Java finds a way that execution could reach the end of a value returning method without reaching a return statement, it causes a compiler error.

Overloaded methods

Since this declaration is in another class, it’s fine to create a max() method even though there’s already one in the Math class. However, it’s possible to create more than one method with the same name even in the same class, provided that their signatures are not the same. Two methods have the same signature if they have the same name and parameter types.

public static int max(int a, int b, int c) {
    return max(max(a, b), c);
}

In this example, we’ve created yet another max() method, but this one takes three parameters instead of two. This method even calls the two parameter version of max(). Creating more than one method with the same name is called overloading those methods. Overloading methods is useful because it allows you to use the same method name for similar functionality, even when there are some underlying differences in the implementation. For example, the Math class provides four different, overloaded versions of the max() method, specialized for int, long, float, and double values, respectively.

There are limitations on creating overloaded methods, of course. The compiler must be able to determine which method you intend to use. Thus, the signatures have to vary by type or number of parameters. A different return type is not enough.

8.3.2. Calling methods

After a method has been defined, it must be called before it does anything. You have plenty of experience calling static methods like Math.sqrt() and Math.max(). An example of the appropriate syntax was given earlier.

int maximum = Math.max(5, 10);

Formally, the call starts with the name of the class (Math), followed by a dot, followed by the name of the method (max), followed by the list of arguments inside parentheses. These arguments are the values you want to pass into the method. Some books use the term formal parameters to describe the variables defined in the method signature and actual parameters to describe the values passed into the methods, but we stick with the simpler terms parameters and arguments.

Of course, the number of arguments must match the number of parameters defined by the method, and the types must match as well. Java performs automatic casting when no precision is lost. Thus, you can always supply an int argument for a double parameter but not the reverse. Arguments can be literal values, variables, or even other method calls that return the appropriate type.

Using the max() method defined before, we could rewrite our simple example without a class name.

int maximum = max(5, 10);

Whenever you call a static method from code that’s inside the same class, you can leave out the class name.

Binding

Many new programmers are confused about the relationship between arguments and parameters. The process of supplying an argument to be used as a parameter is called binding. Through binding, a value or variable from the calling code is given a new name inside of a method. Consider the following method.

public static int add(int a, int b) {
    return a + b;
}

This absurdly short method adds two numbers together and returns the result, approximating the functionality of the + operator. We could call the method in the following context.

int x = 3;
int y = 5;
int z = add(x, y);

Inside the method, the value of x is bound to the variable a, and the value of y is bound to the variable b. The add() method has its own scope. Scope means the area where a variable name is visible (or meaningful). Thus, x and y do not exist inside of the add() method, only the variables a and b do. Since methods have their own scope, variables in one method can have the same names as variables in another method without the compiler (or the programmer!) becoming confused. Consider the following example.

int a = 3;
int b = 5;
int c = add(b, a);

Here the variables a and b exist in both the calling code and inside the method, but the names are independent. The value of a in the calling code happens to be bound to a variable called b inside the method, but the JVM has no confusion about which a is which. Herein lies the value of methods: They are largely independent of whatever else is going on in the code, allowing the programmer to focus on a small, manageable task.

Another important feature of Java is that the process of binding variables is pass by value, meaning that only the value of the argument is bound to the parameter. Whenever a method is called, the method creates a new variable for each parameter and copies the value of its argument into it. In practice, this approach means that a method cannot directly change the value of an argument. Consider the following method.

public static void increment(int counter) {
    counter++;
}

This method takes the value of its argument and copies it into the new variable counter. Then, it increments counter, but the original argument is unchanged. Thus, the following fragment is an infinite loop.

int i = 0;
while(i < 100)
    increment(i);

The value of i remains fixed at 0 for the entire program. The copy of i bound to counter increases to 1 every time increment() is called, but i remains unaffected.

This is not to say that a method cannot affect the variables outside of itself. The primary way that it can do so is by using return statements. We can rewrite increment() to achieve this effect.

public static int increment(int counter) {
    return counter + 1;
}

Then, we need to adjust the loop so that it stores the returned value instead of dropping it on the floor.

int i = 0;
while(i < 100)
    i = increment(i);

A second way that methods can affect the values of outside variables is more indirect. In Java, every argument is passed by value, even arrays and objects. Practically, this means that, if a reference to an array is passed into a method, you cannot change which array it is pointing at. Since references are not values but names pointing at particular locations in memory, you can directly change the contents of that memory with a method, even if you can’t change which locations are being referenced. For example, the following method does not reverse the order of an array.

public static void badReverseArray(int[] array) {
    int[] temp = new int[array.length];
    for(int i = 0; i < array.length; i++)
        temp[i] = array[array.length - i - 1];
    array = temp;
}

Although this code does store a reversed version of array in temp, the last line of the method is meaningless: The array passed into the method still points to the original location in memory. We can rewrite the method to do the reversal in place, meaning that the values of the array are shuffled around, but the array still occupies the same memory locations.

public static void goodReverseArray(int[] array) {
    int temp;
    for(int i = 0; i < array.length / 2; i++) {
        temp = array[i];
        array[i] = array[array.length - i - 1];
        array[array.length - i - 1] = temp;
    }
}

In this version of the method, we swap the first element of the array with the last, the second with the second to last, and so on. We only go up to the halfway point of the array, otherwise we would undo the reversal process. The values of the array are reversed, but they still occupy the same chunk of memory. It’s possible to write a correct method more in the style of badReverseArray() which creates a temporary array, copies the original values into it, and then copies them back to the original array in reverse order, but it’s less efficient to create the extra array and perform two copies.

8.3.3. Class variables

According to the rules we’ve given so far, the only legal variables in the scope of a static method are the parameters and any other local variables declared inside the method. However, it’s possible to create a variable that exists outside of static methods yet is visible inside all of them. These kinds of variables are called class variables (or sometimes static fields or global variables). These variables persist between method calls. The syntax for creating such a variable is to declare it outside of all methods (but inside the class) with the keyword static and an access modifier such as public or private. For example, the following class includes a method called record() that increases the class variable counter every time it’s called.

Program 8.1 Keeps track of the number of times the record() method is called.
public class Bookkeeper {
    public static int counter = 0;

    public static void main(String[] args) {
        while(Math.random() > 0.001)
            record();

        System.out.println("Record was called " + counter + " times.");
    }

    public static void record() {   
        counter++;  
    }
}

When run, this program calls the record() method some random number of times, and the variable counter keeps track of the number. Because counter is static, it’s accessible to both the main() and record() methods. Many programmers frown on the use of class variables precisely because they’re visible to many different methods. The idea of a method is to isolate pieces of code so that the complexity of a program can be divided into simple units. In the case of a public class variable, even code in other classes can modify its value. If many different pieces of code can modify a variable, it may be difficult to keep that variable from being changed in unexpected ways. For example, if another method used the counter variable to record the number of times it was called, the final value of counter would be the sum of the number of times the two methods were called. There might be some reason to keep track of such information, but it would be impossible to reconstruct what fraction of the value in counter came from one method and what fraction came from the other.

Class variables have their uses, but they should generally be avoided. The chief exception to this rule is constants. Since a constant never changes, a class variable is a great place to store it, making the value available to all code. An example you’ve already used is Math.PI. As with static methods, a static field from another class can be accessed by using the class name, then a dot, and then the name of the static field. Again, when the code using the field is in the same class, the class name can be dropped. A class constant is declared like a class variable, but with the addition of the final keyword.

Example 8.1 Gravitational attraction

The following class allows a user to compute the one-dimensional force due to gravity. This force F is given by the following equation.

gravitation

In this equation, m1 is the mass of one object, m2 is the mass of another, r is the distance between their centers, and G is the gravitational constant, 6.673 × 10-11 N⋅m2⋅kg-2.

Program 8.2 Calculates the attraction due to gravity between two masses.
import java.util.Scanner;

public class Gravity {
    public static final double G = 6.673e-11;

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("What is the first mass?");
        double m1 = in.nextDouble();
        System.out.println("What is the second mass?");
        double m2 = in.nextDouble();
        System.out.println("What is the distance between them?");
        double r = in.nextDouble();
        System.out.println("The force of gravity is " + force(m1, m2, r) + " N");
    }

    public static double force(double m1, double m2, double r) {    
        return G*m1*m2/(r*r);
    }
}

Use named constants whenever they increase readability. You can use the public modifier if you want all classes to have access to your constant (Gravity.G is a good example). You can use the private modifier if you want the constant to be accessible only inside your class, if it has no use outside, or if it contains secret information.

8.4. Examples: Defining methods

Any large problem should be broken down into methods. Because the technique is useful in so many circumstances, it’s difficult to give a set of examples that covers all cases. Instead, our examples are short, easy to understand methods, focusing on Euclidean distance, testing for palindromes, and converting a String representation of an int to an int.

Example 8.2 Euclidean distance

The Euclidean distance between two points is the length of a straight line connecting them. It plays an important role in 3D graphics and games and is the basis for many other practical applications involving spatial relationships.

Given two points in 3D space (x1, y1, z1) and (x2, y2, z2), we can compute the Euclidean distance between them with the following equation.

distance

The method below applies this equation directly.

public static double distance(double x1, double y1, double z1,
						double x2, double y2, double z2) {
    double x = x1 - x2;
    double y = y1 - y2;
    double z = z1 - z2;
    return Math.sqrt(x*x + y*y + z*z);
}

This equation is a good candidate for a static method since it might be necessary to do this calculation many times and it does not depend on any other variables or program state.

Example 8.3 Palindrome testing

A palindrome is a word or phrase (or even a number) that’s the same spelled forward and backward. “Racecar,” “Madam, I’m Adam,” and “Satan, oscillate my metallic sonatas” are examples in English. Typically, spaces and punctuation are ignored. We’re going to write a function that, given a String, returns true if it’s a palindrome and false otherwise. To simplify the problem, we’re not going to ignore spaces and punctuation. Thus, with our method, “racecar” counts as a palindrome, but neither of the other two examples would.

public static boolean isPalindrome(String text) { (1)
    text = text.toLowerCase(); (2)
    for(int i = 0; i < text.length() / 2; i++) (3)
        if(text.charAt(i) != text.charAt(text.length() - i - 1)) (4)
            return false;
    return true;
}
1 Because our method returns true or false, its return type must be boolean. Many methods that return a boolean value have a name starting with is, like our method.
2 The first line of the body of our method changes text to lowercase. The String method toLowerCase() creates a lowercase copy of the String it’s called on, in this case text. Then, we point the reference variable text at that new, lowercase String. On the outside of this function, the String passed in does not change because the reference text is passed by value.
3 The loop iterates through the first half of text, comparing it to the second half. This loop reflects the asymmetry of these kinds of tests: You can’t be sure that text is a palindrome until you’ve checked the entire thing, but you immediately know that it’s not if even a single pair of characters doesn’t match.
4 If the test in that if statement ever shows that the two char values aren’t equal, the return false statement jumps out of the method without completing the loop.
Example 8.4 Parsing a number

When you read in a number using an object of the Scanner class, it converts (or parses) the text entered by the user into the appropriate type. For example, the nextDouble() method reads in some text and convert it into a double. When you use a JOptionPane method to read in input, it comes in as a String. If you want to use that data as a double, you must convert it using the Double.parseDouble() static method. Some Java programmer had to write this method. We’re going to recreate a similar method to convert the String representation of a floating-point number into a double. Our simple method ignores scientific notation.

public static double parseDouble(String value) {
    int i = 0;
    boolean negative = false;
    double temp = 0.0;
    double fraction = 10.0;
    if(value.charAt(i) == '-') { (1)
        negative = true;
        i++;
    }
    else if(value.charAt(i) == '+')
        i++;
    while(i < value.length() && value.charAt(i) != '.') { (2)
        temp *= 10.0;
        temp += value.charAt(i) - '0';
        i++;
    }
    i++; //move past decimal point (if there) (3)
    while(i < value.length()) { (4)
        temp += (value.charAt(i) - '0') / fraction;
        fraction *= 10.0;
        i++;
    }
    if(negative) (5)
        temp = -temp;
    return temp;
}
1 After declaring a few variables, this method first checks index 0 in the input String value to see if it is a '-' or a ''`. If it is a `'-'`, it sets `negative` to `true` and moves on. If it is a `'', it simply moves on.
2 Then, the method loops through value until it reaches the end or reaches a decimal point. As it iterates, it multiplies the current value of temp by 10.0 and adds in the next digit from value (after subtracting '0' so that the range is is from 0 to 9). This repetitive multiplication by 10.0 accounts for the increasing powers of 10 in the base 10 number system. Since temp starts with a value of 0.0, the first multiplication has no effect, as intended.
3 After the first while loop, the index i is incremented once, to skip the decimal point, if there is one. If there is no decimal point, the loop must have exited because the end of value had been reached.
4 The second while loop runs to the end of value, this time adding in each digit value divided by fraction, which is increased by a factor of 10.0 each time. Doing so allows us to add smaller and smaller fractional digits to the total.
5 We set temp to its opposite if the flag negative was set earlier and finally return temp.

Note that the real Double.parseDouble() method not only accepts String values in scientific notation but also does a great deal of error checking. Our code either crashes or gives inaccurate results on an empty String, a String containing non-numerical characters, or a String with more than one decimal point. Furthermore, this code does not use the best approach for minimizing floating-point precision errors.

8.5. Solution: Three card poker

Here we present our solution to the three card poker problem. We explain each method individually.

public class ThreeCardPoker {
    public static final String[] SUITS = {"Spades", "Hearts", (1)
        "Diamonds", "Clubs"};
    public static final String[] RANKS = {"2", "3", "4", "5", "6",
        "7", "8", "9", "10", "Jack", "Queen", "King", "Ace"};   
    public static final int STRAIGHT_FLUSH = 40; (2)
    public static final int THREE_OF_A_KIND = 30;
    public static final int STRAIGHT = 6;
    public static final int FLUSH = 2;  
    public static final int PAIR = 1;
    public static final int NOTHING = 0;
1 Before our main() method begins, we declare a number of class constants. Two constant arrays of String values provide us with an easy way to represent suits and ranks.
2 The remaining six int constants are used to allocate a winning payoff to each possible outcome.

Note that these constants can be declared anywhere inside the class, provided that they’re outside of methods. However, it’s typical (and good style) to declare them at the top of the class.

    public static void main(String[] args) {
        int[] deck = new int[52]; (1)
        int[] hand = new int[3];                
        for(int i = 0; i < deck.length; i++) (2)
            deck[i] = i;
        shuffle(deck);
        for(int i = 0; i < hand.length; i++) (3)
            hand[i] = deck[i];      
        int winnings = score(hand); (4)
        System.out.println("Hand: ");
        print(hand);
        if(winnings == 0) (5)
            System.out.println("You win nothing.");
        else
            System.out.println("You win " + winnings +
            " times your bet.");
    }
1 In the main() method, an array representing a deck of 52 cards is created first, followed by an array representing the 3 cards to be dealt.
2 The deck is filled sequentially and then shuffled with a method.
3 Next, the first 3 cards of the deck are copied into the array representing the hand of cards.
4 The score of the hand is determined, and then the hand is printed out. We print the hand after determining the score because the hand is sorted in the process of determining the score, making the output easier to read.
5 Finally, we print the appropriate output, depending on the score.
    public static void shuffle(int[] deck) {
        int index, temp;
        for(int i = 0; i < deck.length; i++) {
            index = i + (int)((deck.length - i)*Math.random());
            temp = deck[index];
            deck[index] = deck[i];
            deck[i] = temp;
        }       
    }

This method shuffles the deck. Its approach is to swap the first element in the array of cards with one of the elements that follow, chosen randomly. Then, it swaps the second element in the array with any of the elements that follow it, and so on. If Math.random() truly gives us a uniformly generated random number in the range [0,1), the final shuffled deck should be any one of the 52! possible decks with equal probability.

    public static void print(int[] hand) { (1)
        for(int i = 0; i < hand.length; i++)
            System.out.println(RANKS[getRank(hand[i])] + " of "
            + SUITS[getSuit(hand[i])]);
    }

    public static int getRank(int value) { return value % 13; } (2)
    public static int getSuit(int value) { return value / 13; } (3)
1 The first of these methods prints out a human readable version of each card in an array (instead of 0 - 51). It does so using the second and third methods as helper methods.
2 Method getRank() computes the rank of a card from its number.
3 Method getSuit() computes the suit of a card from its number.

The indexes obtained from these methods are used to index into the RANKS and SUITS arrays.

In the C language, calling the equivalent of a method from a method defined earlier requires a special declaration step called prototyping before both methods. Java does not have this complication, and the getRank() and getSuit() methods compile and function perfectly if they are written above print() or below it inside the class definition.

    private static int score(int[] hand) {  
        sortByRank(hand); 
        if(hasStraight(hand) && hasFlush(hand))
            return STRAIGHT_FLUSH;
        if(hasThree(hand))
            return THREE_OF_A_KIND;
        if(hasStraight(hand))
            return STRAIGHT;
        if(hasFlush(hand))
            return FLUSH;
        if(hasPair(hand))
            return PAIR;        
        return NOTHING;
    }   

This method computes the score by first sorting the hand and then testing progressively worse outcomes, starting with the best, a straight flush. As it moves down the list of outcomes, it calls appropriate methods to determine if a hand has a certain characteristic.

    private static void sortByRank(int[] hand) {
        int smallest, temp;
        for(int i = 0; i < hand.length - 1; i++) {
            smallest = i;
            for(int j = i + 1; j < hand.length; j++) {
                if(getRank(hand[j]) < getRank(hand[smallest]))
                    smallest = j;
            }
            temp = hand[smallest];
            hand[smallest] = hand[i];
            hand[i] = temp;
        }
    }

This code is an implementation of selection sort packaged into a method. Note that this method does change the values inside of the array hand even though it cannot change the array that hand points to. The array itself is passed by value, but its contents are effectively passed by reference.

    private static boolean hasPair(int[] hand) { (1)
        return getRank(hand[0]) == getRank(hand[1]) ||
               getRank(hand[1]) == getRank(hand[2]);
    }

    private static boolean hasThree(int[] hand) { (2)
        return getRank(hand[0]) == getRank(hand[1]) &&
               getRank(hand[1]) == getRank(hand[2]);
    }

    private static boolean hasFlush(int[] hand) { (3)
        return getSuit(hand[0]) == getSuit(hand[1]) &&
               getSuit(hand[1]) == getSuit(hand[2]);
    }

    private static boolean hasStraight(int[] hand) { (4)
        return (getRank(hand[0]) == 0 && getRank(hand[1]) == 1
                && getRank(hand[2]) == 12) || //ace low
               (getRank(hand[1]) == getRank(hand[0]) + 1 &&
                getRank(hand[2]) == getRank(hand[1]) + 1);
    }   
}
1 The code in hasPair() works by checking to see if the first and second or second and third cards have the same rank. An extra condition would be required if the cards weren’t sorted.
2 The code in hasThree() checks to see if all the ranks are the same.
3 The code in hasFlush() is the same as hasThree() except that it checks for suit instead of rank.
4 Finally, hasStraight() checks to see if the ranks come one after the other, with an extra case to deal with the possibility of the ace counting as low. This test only works because the cards were sorted previously.

These four methods would be similar but more complex for five- or seven-card poker hands.

8.6. Concurrency: Methods

In Java, it’s impossible to have concurrency without methods. Methods are the way we break a large program into manageable pieces but are also part of the syntax that Java uses to create threads of execution. Each thread of execution is associated with a Thread object; however, creating the object is not enough to start a new thread of execution running. Only when the start() method is called on the Thread object does the new thread start running.

Hopefully, you’ve begun to visualize the execution of Java programs as an arrow that sits next to each line of code as it’s executed. This arrow can jump to a choice and skip over other code using if and switch statements. Using loops, the arrow can jump backward and repeatedly execute code it’s just executed. As we discussed in this chapter, the arrow can jump into a method, execute the code in that method, and then return to its caller, going back right to where it left off before the call.

When the start() method is called on a Thread object, however, the arrow returns to the caller, but it also splits itself into a second arrow that then executes the corresponding run() method and any other methods it calls. Note that we’re talking about a method called on a Thread object, not a static method called on the class as a whole. Calling start() is an instance method, which we discuss in Chapter 9. Unlike the static methods in this chapter, an instance method is tied to a particular object, but most of what you’ve learned about methods still applies.

Methods are supposed to make programming easier by breaking programs into chunks small enough to think about. One of the only real dangers of methods is using class variables, as mentioned in Section 8.3.3. This problem becomes worse with multiple threads. With a single thread, two or more different methods can all affect the same class variable, perhaps in conflicting ways. With multiple threads, even the same method can interfere with itself.

Example 8.5 LCG without thread safety

A linear congruential generator (LCG) allows you to create a sequence of pseudorandom numbers using the equation xi = (axi-1 + b) mod m, deriving the next number from the previous one, and so on.

Program 8.3 Implements an LCG similar to one sometimes used in the function rand() used in the C language.
public class UnsafeRandom {
    private static int next = 1;
    private final static int A = 1103515245;
    private final static int B = 12345;
    private final static int M = 32768;
    
    public static int nextInt() {
        return next = (A*next + B) % M;
    }
}

The UnsafeRandom program listed above always generates the same sequence of pseudorandom numbers, which can be useful for debugging a program. However, if two or more threads call nextInt(), they’ll probably get different sequences. One thread will pick up some of the numbers, and the other will pick up the missing numbers in between. If each thread wants to generate the same sequence of numbers, the method should be rewritten so that it takes in the previous number in the sequence. In that way, there’s no shared state. Remember that using a (non-final) class variable should be avoided whenever possible.

Program 8.4 Implements the same LCG safely by requiring the caller to supply the previous random number.
public class SafeRandom {   
    private final static int A = 1103515245;
    private final static int B = 12345;
    private final static int M = 32768;
    
    public static int nextInt(int previous) {
        return (A*previous + B) % M;
    }
}
Example 8.6 Unpredictable methods

By forcing each thread to carry its own state, we fixed the previous problem. In Chapter 14 we’ll talk about the much nastier problem of two threads executing a method at exactly the same time. When that happens, very curious effects are possible. Consider the following program.

Program 8.5 The print() method always prints "Even" when run with a single thread but can sometimes print "Odd" if called repeatedly with multiple threads.
public class AlwaysEven {
    private static int value = 1;

    public static void print() {        
        value++; (1)
        if(value % 2 == 0)
            System.out.println("Even");
        else
            System.out.println("Odd");
        value++; (2)
    }
}
1 With a single thread running, value always goes up to an even number before printing.
2 Afterward, it increments to the next odd number.

If two or more threads call the print() method, value could be changed by one right before the other executes the if statements.

8.7. Exercises

Conceptual Problems

  1. Describe three advantages of dividing long segments of code into static methods.

  2. Can you think of any disadvantages of dividing code into methods? Are there situations in which using a method is unwise?

  3. If you wanted to declare a static method that would compute the mean, median, and standard deviation of an input array of double values, how would you return those three answers?

  4. Consider the following method definition.

    public static void twice(int i) {
        i = 2 * i;
    }

    How many times does the following loop run, and why?

    int x = 2;
    while(x < 128)
        twice(x);
  5. Consider the following signatures of two overloaded methods.

    public static int magic(int rabbit, double hat)
    public static int magic(double wand, int spell)

    Which method would be invoked by the following call?

    int x = magic(3, 16);

    What about the following?

    int y = magic(3.2, 16.4);

    Use a compiler to check your answers.

  6. The following class generates a sequence of even numbers. Each time the next() method is called, the next even number in the sequence is returned. What’s the design problem with using a static field to keep track of the next value in the sequence?

    public class EvenNumbers {
        private static int counter = 0;
    
        public static int next() {
            counter += 2;
            return counter;
        }
    }

Programming Practice

  1. Write a static method called cube() that takes a single double value as a parameter and returns its value cubed. Do not use the Math.pow() method.

  2. Implement a static method that takes a single int value as a parameter and prints its digits in reverse. For example, if 103 was passed into this method, it would print 301 to the screen.

    You can find out what digit is in the ones place of a number by taking its remainder modulus 10. Then, you can remove the digit in the ones place by dividing by 10. Do not convert the int value into a String.

  3. Write a static method that takes an array of int values as a parameter and returns true if the array is in ascending order and false otherwise. Compare each element of the array to the next element of the array. If the current element is ever larger than the next element, the array is not sorted in ascending order. Note that you can only be sure that the array is in ascending order after you have checked all neighboring pairs.

  4. Write a static method that finds the ⌊log2 n⌋ of an integer n. Note that if log2 n = x, it’s also true that n = 2x. In other words, the log2 operator tells you what power of 2 a number is. One way to define the log2 n is the number of times you have to divide n by 2 to get 1. Use this definition to make a loop that finds the value without using any calls to the Math library.

    Here are some examples of the return values your method should give for various input values of n.

    n Return Value n Return Value

    1

    0

    16

    4

    2

    1

    100

    6

    4

    2

    512

    9

    8

    3

    1000

    9

    10

    3

    1024

    10

  5. Write a method that tests palindromes like the method from Example 8.3 but also ignores punctuation and spaces. Thus, "A man, a plan, a canal: Panama" should be counted as a palindrome by this new method.

  6. Add to the parseDouble() method from Example 8.4 so that it can also handle numbers in standard Java scientific notation, such as 7.239e-14. Note that the e can be uppercase or lowercase, and the exponent can begin with a minus sign (-), a plus sign (+), or neither.

  7. Re-implement the solution from Section 8.5 so that it uses a GUI constructed with JOptionPane to display the hand and the winnings.

  8. Five card poker is a much more common version of poker than the three card version we discussed in Section 8.1. Using static methods, implement a two-player game of poker in which the deck is shuffled and then dealt into two hands of five cards each. Then, state which player’s hand wins. With five cards, determining which hand wins is a more complicated process. The rankings of the various possible hands from best to worst are as follows.

    Straight Flush

    All five cards belong to the same suit and have ranks in sequential order (with either ace high or low). If two people both have straight flushes, the higher ranked one wins. If they both have the same ranks, it is a tie.

    Four of a Kind

    Four of the five cards have the same rank. If two people have four of a kind, the higher rank set of four wins.

    Full House

    Three of the five cards have the same rank and the other two share another rank. If two people have a full house, the higher ranked set of three wins.

    Flush

    All five cards have the same suit. If two people have flushes, the one with the highest card wins. If the highest card is a tie, the next highest is the tie breaker, and so on. If the two flushes have exactly the same ranks, the two flushes tie.

    Straight

    All five cards have ranks in sequential order (with either ace high or low). If two people both have straight, the higher ranked one wins. If they both have the same ranks, it is a tie.

    Three of a Kind

    Three of the cards have the same rank. If two people have three of a kind, the higher ranked set of three wins.

    Two Pair

    A pair of cards have the same rank and another pair of cards share another rank. If two people both have two pairs, the higher ranked pair is a tiebreaker. If the higher ranked pair is the same, the lower ranked pair is a tiebreaker. If the lower ranked pair is the same, the final unpaired card is the tiebreaker. If all the ranks of both hands match, it is a tie.

    Pair

    A pair of cards has the same rank. If two people have pairs, the rank of the pair is a tiebreaker. If the pairs have the same rank, the remaining cards in each hand are tiebreakers, in descending rank order.

    High Card

    If none of the other cases hold, the high card determines the value of the hand. If two people have the same highest card, the remaining cards in each hand are used as tiebreakers, in descending rank order.

Experiments

  1. In terms of time, there’s a small overhead associated with calling a method and returning a value, but it’s very hard to measure. Write a program with two int variables, a and b, where a starts with a value of 1 and b starts with a value of 2. Run a for loop 100,000,000 times. On each iteration first increase the value of a by the value of b and then increase the value of b by a. Time this loop with System.nanoTime(), and then print out the time taken and the value of a. The value of a is not important, but the compiler will optimize away the math done with a and b unless we output the value. We recommend that you run this program repeatedly to get a sense of the average running time.

    Now, instead of using the + operator to add a and b, use the following method.

    public static int add(int a, int b) {
        return a + b;
    }

    Again, run your program repeatedly with this modification. What’s the difference in running time between the version that uses a method and the version that does addition directly?

    Depending on your JVM, it’s quite possible that there’s almost no difference. The JVM does a lot of optimizations including inlining, which replaces a call to a short method with the actual code inside the method.

9. Classes

Luminous beings are we, not this crude matter.

— Yoda

9.1. Problem: Nested expressions

How does the compiler check the Java code that you write and find errors? Parsing and type checking are involved processes that are key parts of compiler design. Compilers are some of the most complex programs of any kind, and building one is beyond the scope of the material covered in this book. However, we can get insight into some problems faced by compiler designers by considering the problem of correctly nested expressions.

There are many rules for forming correct Java code, but it’s always the case that grouping symbols ((, ), [, ], {, }) must be correctly nested. Ignoring other symbols, we may find a section of code that contains the sequence ( ) [ ], but correctly written code never contains ( [ ) ].

To be correctly nested, left and right parentheses must be balanced, left and right square brackets must be balanced, and left and right curly braces must be balanced. Furthermore, a correctly balanced set of parentheses can be nested inside of a correctly balanced set of square brackets or curly braces (and vice versa), but they cannot intersect as they do at the end of the previous paragraph. The table below shows more examples of correctly and incorrectly nested expressions.

Correctly Nested Incorrectly Nested
(
(){}
}{
((abc)){x}
( a { b ) c }
({[z]})
{abc(
({xyz})({ijk}[123])
{(333)888(})

But how can we examine an expression to see if it’s correctly nested? The key to solving this problem is the idea of a stack. A stack is a simple data structure with three operations: push, pop, and top. The data structure is meant to behave like a stack of books or cups or any other physical objects. When you use the push operation, you’re moving something to the top of the stack. When you use the pop operation, you’re taking something off the top of the stack. The top operation is used to read what’s currently at the top of the stack. In a stack, you can only read that very topmost item, as if all the other items were buried underneath it. A stack is known as a FILO (first in, last out) or a LIFO (last in, first out) data structure because, if you push a series of items onto the stack and then pop them all off the stack, they come off the stack in the reverse order from how they were added.

Armed with an understanding of the stack, it is straightforward to see if an expression is nested correctly. Scan through each character in the input and follow these steps.

  1. If it’s a left parenthesis, left square bracket, or left curly brace, put it on the stack.

  2. If it’s a right parenthesis, right square bracket, or right curly brace, check that the top of the stack is its matching left half. If it is, pop the left half off the stack. If it isn’t (or if the stack is empty), the grouping symbols are either unbalanced or intersecting. Print an error and quit.

  3. For any character that isn’t a grouping symbol, ignore it and move on.

If you reach the end of input without an error, check to see if the stack is empty. If it is, then all of the left grouping symbols have been matched up with right grouping symbols. If not, there are left symbols left on the stack, and the expression isn’t correctly nested.

9.2. Concepts: Object-oriented programming

To solve the nested expressions problem, we can create a stack data structure. Each element of the stack should be able to hold a char value term from an expression, where a term is an operator, operand, or a parenthesis (bracket or curly brace). Although we could get by using only the static methods we introduced in the previous chapter, a better tool can make programming easier.

We’re introducing these topics in a progression: First, all of our programs were a series of sequential instructions inside of a main() method. We added in selection statements and loops to solve more difficult problems. As our programs became more complicated, we started using additional static methods to divide the program into logical segments. Now, we’re moving to fully object-oriented programming (OOP) in which data as well as code can be packaged into objects that interact with each other.

OOP has been a controversial topic, particularly in the computer science education community. The most important thing to remember is that we’re not throwing away any of the ideas we used before. We’re continuing on a path toward making code safer and more reusable. Several other chapters in this book touch on important ideas in OOP such as inheritance and polymorphism. Below, we’re going to focus on the basics, including the fundamentals of objects, encapsulation of data, and instance (non-static) methods.

9.2.1. Objects

You’ve already used objects, perhaps without realizing it. Every time you use a String, you’re using an object. So far, we’ve created a class every time we’ve written a program. A class is a way to organize static methods, but a class is something more: a template for objects. Whenever you write a class, the potential to create objects from it exists.

For a conceptual example, you can think of Human as a class and Albert Einstein as an object (or an instance) of that class. The class defines certain characteristics that all human beings have: name, date of birth, height, and so on. Then, the object has specific values for each one of those characteristics, such as Albert Einstein, March 14, 1879, 175, and so on.

class
Figure 9.1 Example of a class serving as a template for an object.

This idea of a class as a template is key because it means that an object of a given type (from a given class) can be used anywhere that’s appropriate for another object of that type. Later in the chapter, we’ll create an object that performs the work of a stack, storing a number of other objects. If we design a library of code that can manipulate or use stack objects, we should be able to use this library countless times for countless different stack objects without changing the code. This kind of code reuse is one of the main goals of OOP.

9.2.2. Encapsulation

In order to guarantee that objects can be used in many different contexts safely, the data inside of the object must be protected. Java provides access modifiers so that code without the appropriate permission can’t change or even read the data inside of an object.

This feature is called encapsulation. One programmer might write the class file defining a type of object while another or many others write code that uses those objects. The programmers who use objects written by others do not need to understand the inner workings of those objects. Instead, they can treat each object as a “black box” with a list of actions that the object can do. Each action has a certain specified input and a certain specified output, but the internal functioning of the object is hidden.

9.2.3. Instance methods

These “actions” are methods but not static ones. Static methods did not need an object in order to be called. Regular (instance) methods should be thought of as an action performed on (or by) a specific object. This action could be asking a question, such as inquiring what the name of an object of type Human is. This action could be telling the object to change itself, as in the case of pushing something onto a stack.

One of the broadest definitions of an object is a collection of data and methods to access that data. We call the data inside of an object its fields or instance data and the methods to access them instance methods. Static methods are used primarily to modularize large blocks of code into smaller functional units. However, instance methods are tightly coupled to the fields of an object and perform tasks that change the object or get information from it.

9.3. Syntax: Classes in Java

OOP concepts such as encapsulation may seem esoteric until you see them in practice. Remember, we just want to create some private data and then define a few carefully controlled ways that the data can be manipulated. First, we’ll describe how to declare fields, then explain how to write instance methods to manipulate that data, and finally give more details about protecting its privacy.

9.3.1. Fields

Fields in an object must be declared like any other data in Java. The type of a field can be a primitive or reference type. Fields are also sometimes called member variables. You declare fields just like you would class variables, except without the static keyword. Here’s an example with the Human class.

public class Human {
    private String name;
    private String DOB;
    private int height; // in cm
}

With this definition, a Human object has three attributes: name, DOB, and height. Because the access modifier for each field is private, code outside of this class can’t change or even read the values. This class can’t do anything yet. Also, it doesn’t contain a main() method. There’s no way to run this class, but that’s fine. We could add a main() method, of course.

public class Human {
    private String name;
    private String DOB;
    private int height; // in cm

    public static void main(String[] args) {
        name = "Albert Einstein";
        DOB = "March 14, 1879";
        height = 175;
    }
}

Now we’ve added a main() method, but our code doesn’t compile. Since the main() method is a static method, it is not associated with any particular object. When we tell the main() method to change the fields, it doesn’t know what object we’re talking about. If we actually want to use an object, we’ll have to create one.

Program 9.1 Class encapsulating the attributes of a human being.
public class Human {    
    private String name;
    private String DOB;
    private int height; //in cm 

    public static void main(String[] args) {
        Human einstein = new Human();
        einstein.name = "Albert Einstein";
        einstein.DOB = "March 14, 1879";
        einstein.height = 175;      
    }
}

The above code compiles because we’ve used the new keyword to create an object of type Human saved in a reference variable called einstein. We can set the fields inside of a particular object using dot notation. With static methods and static variables, we used the name of the class followed by a dot, but for instance methods and instance variables, we use the name of the object followed by a dot. Even though each of these fields is private, we can access them from main() because main() is inside the Human class. Code inside of another class could create a new Human object, but it could not change its fields.

This juxtaposition of static and non-static fields and methods inside of a single class is confusing to many new Java programmers. The confusion seems to stem from the fact that the class (such as Human) is a template for objects but it’s also a place to house other related code, such as static methods, including main().

Although the practice is discouraged, we mentioned in Section 8.3.3 that class variables can be stored in the class itself. Every object has a distinct copy of each field, but there’s only a single copy of each class variable that they all share. By using the keyword static, we could add a class variable called population to our Human class, since that’s information connected to humans as a whole, not to any individual human being.

public class Human {
    private String name;
    private String DOB;
    private int height; // in cm
    private static long population = 7714576923;
}

We’re using a long to represent the world’s population since the value is too big to fit in an int. If several Human objects were created, they would each have their own name, DOB, and height values, but the value for population would only be stored in the class.

staticvalue
Figure 9.2 Class variables (static fields) are stored with the class, not with individual objects.

9.3.2. Constructors

To create a new object, you have to invoke a constructor, a special kind of method that can initialize the object. A constructor sets up the values inside an object when the object’s first created. Let’s consider a simple Rectangle class with only two fields: length and width, both of type int.

public class Rectangle {
    private int length;
    private int width;

One possible constructor for the class is given below.

    public Rectangle(int l, int w) {
        length = l;
        width = w;
    }

This constructor lets us set the width and length when the object’s created. To do so, code must invoke the constructor using the new keyword.

Rectangle rectangle = new Rectangle(50, 20);

This code creates a new Rectangle object, with length 50 and width 20. Constructors are almost always public; otherwise, it would be impossible for code outside of the Rectangle class to create a Rectangle object. Note that the definition of the Rectangle constructor does not have a return type. A constructor is the only kind of method that doesn’t have a return type. It’s possible to have more than one constructor as well, just as other methods can be overloaded. For more information about overloaded methods, refer back to Section 8.3.1.2.

    public Rectangle(int value) {
        length = value;
        width = value;
    }

In the very same class, we could have this second constructor, allowing us to create a square quickly and easily. All classes have constructors, but some aren’t written explicitly. If you don’t type out a constructor for a class, a default one is automatically created for you. The default constructor takes no parameters and sets all the values inside the new object to defaults such as null and 0. Once you do create a constructor, the default one is no longer provided. Thus, since our definition of the Rectangle class already contains two constructors, the following line would cause a compiler error if someone tries to use it in their code.

Rectangle defaultRectangle = new Rectangle();

Another important thing to consider with all instance methods is scope. Fields are visible inside of instance methods, but they can be hidden by parameters and other local variables.

    public Rectangle(int length, int width) {
        length = length;
        width = width;
    }

This version of the two parameter Rectangle constructor compiles, but it doesn’t properly initialize the values of the fields length and width. Instead, the parameters length and width are copied back into themselves for no reason. The designers of Java anticipated that it would be useful to refer to fields even in the presence of other variables with the same name. To do so, the this keyword can be used. Any field (or method) can be referred to by its object name, followed by a dot, followed by the name of that field or method. Since you don’t have a variable name to reference the object when you’re inside of it, the this keyword acts as a reference to the object.

    public Rectangle(int length, int width) {
        this.length = length;
        this.width = width;
    }

This version of the code functions correctly, since we’ve explicitly told Java to store the argument length into the field length inside the object pointed at by this and to do similarly for width.

9.3.3. Methods

Objects don’t really come to life until you add instance methods. With the Rectangle class described above, any Rectangle objects created would not be useful to other classes because it would be impossible to access their data. Instead, we want to create a clear and usable relationship between the fields and the methods.

There are many different kinds of methods, but two of the most important are accessors and mutators.

Accessors

We often want to read the data inside of various objects. With our current definition of Rectangle, no code from an outside class can find out the length or width of the rectangle we’re representing.

Accessor methods (or simply accessors) are designed for this task. By definition, an accessor allows us to read some data or get some information out of an object without making any changes to its fields. Accessors can be thought of as asking the object a question. The names of accessors often start with the word get.

    public int getLength() {
        return length;
    }

    public int getWidth() {
        return width;
    }

Here are two accessors methods that we’d expect in the Rectangle class. The first returns the value of length, and the second returns the value of width. These methods only report information. They don’t change the value of either variable. Their syntax should be self-explanatory. Each is declared to be public so that anyone can read the length and width of a rectangle. Both methods have a return type of int because that’s the type used to store length and width inside a Rectangle object. Neither method has any parameters. Of course, an accessor doesn’t have to be so simple. An accessor could return a value that needs to be computed from the underlying field data.

    public int getArea() {
        return length*width;
    }

    public int getPerimeter() {
        return 2*length + 2*width;
    }

These accessors compute the area and perimeter, respectively, of the rectangle in question, even though that data isn’t stored directly in the Rectangle object.

Mutators

Some objects, such as String values, are immutable objects, meaning that the data stored inside them cannot be changed after they’ve been created with a constructor. If you’ve ever thought you were changing a String, you were actually creating a new String with the appropriate modifications. Most objects are mutable, however, and we use methods called mutator methods (or simply mutators) to change their fields.

Like accessors, mutators have no special syntax. The term is used to describe any methods that change the data inside of an object. For the Rectangle class, the only internal data we have is the length and width variables. Mutators for these might look as follows.

    public void setLength(int length) {
        this.length = length;
    }

    public void setWidth(int width) {
        this.width = width;
    }

Just as the names for many accessors begin with get, the names for many mutators begin with set. Mutators often have a void return type because they’re changing the object, not getting information back. Some mutators might have a return type that gives information about an error that occurred while trying to make a change. Note that we used the this keyword once again to distinguish each field from the method argument with the same name.

You may have noticed that we use the machinery of a method to both get and set the length field, for example. Perhaps doing so seems needlessly complex. After all, if the length variable had been declared with the public modifier instead of the private modifier, we could get and set its value directly, without using methods. In response, let’s improve the mutators that set length and width.

    public void setLength(int length) {
        if(length > 0)
            this.length = length;
    }

    public void setWidth(int width) {
        if(width > 0)
            this.width = width;
    }

With these better mutators, we can prevent a user from setting the values of length and width to negative numbers or zero, values that don’t make sense for dimensions of a rectangle. For more complicated objects, it becomes even more important to protect the values of the fields from malicious or mistaken users.

9.3.4. Access modifiers

Hiding data is at the heart of the Java OOP model. There are four different levels of access that can be applied to fields and methods, whether static or not. They are public, private, protected, and package-private.

public modifier

The public access modifier states that a variable or method can be accessed by any code, no matter what class contains it. Most methods should be public so that they can be used freely to interact with their object. Virtually no fields should be public. Constants (static or otherwise) are the most significant exception to this rule. Making constants public is usually not a problem since they can’t be changed by outside code anyway. In the Rectangle class, variables length and width are so simple that making them public is not unreasonable. If you have a field that can be changed at any time by any code to any value, you can leave that field public.

private modifier

This modifier states that a variable or method cannot be accessed by any code unless the code is contained in the same class. It’s important to realize that the restriction is based on the class, not on the object. Code inside any Rectangle object can modify private values inside of any other Rectangle object and the class as a whole. Most fields should be private so that outside code can’t modify them. Methods can be private, but these methods should be helper or utility methods used inside the class or object to divide up work.

protected modifier

This modifier states that a variable or method cannot be accessed by any code unless the code is contained in the same class, a subclass, or is in the same package. This level of access is more restrictive than public but less restrictive than private or default access. We discuss it further in the context of subclasses and inheritance in Chapter 11.

Package-private (no explicit modifier)

If you don’t type an access modifier when you declare a field or method, that field or method is not public. Instead, it has the default or package-private access modifier applied to it. Fields or methods with this modifier can be accessed by any code that is in the same package or directory. A package is yet another layer of organization that Java provides to group classes together. When you use an import statement, you can import an entire package of classes. There’s no keyword for this access modifier. It may be useful if you’re designing a package containing classes that must be able to access each other’s fields or methods. For now, you should always give your fields and methods an explicit public or private (or sometimes protected) modifier.

From least restrictive to most restrictive, the modifiers are public, protected, package-private, and private. Each additional level of restriction removes a single category of access. All fields and methods can be accessed by code from the same class. The following table gives the contexts outside the class that can access a field or method marked with each modifier.

Modifier Package Subclass Unrelated
Classes

public

Yes

Yes

Yes

protected

Yes

Yes

No

Package-private

Yes

No

No

private

No

No

No

Although large and complex programs are needed to see the real benefits of OOP in Java, here’s an example showing how objects can be used to make a roster of students.

Example 9.1 Student roster

We’re going to create a Student class so that we can store objects containing student roster information. Then, we’re going to create a client program that reads data from a user to create Student objects, sort them by GPA, and then print them out.

public class Student {
    public static final String[] YEARS = {"Freshman", "Sophomore", "Junior", "Senior"};
    private String name;
    private int year;
    private double GPA;

We start by defining the Student class. First, there’s a constant array of String values, giving the names of each of the four years. Next, fields in the Student class are declared to store the name, year, and GPA of the student.

    public Student(String name, int year, double GPA) {
        setName(name);
        setYear(year);
        setGPA(GPA);
    }   

We have one constructor for this class, which takes in a String, an int, and a double corresponding to the name, year, and GPA of the student. The constructor then internally uses mutator methods to store the values into the fields. By doing so, we automatically take advantage of the error checking in the GPA mutator.

    public void setName(String name) { this.name = name; }  
    public void setYear(int year) { this.year = year; }

    public void setGPA(double GPA) {
        if(GPA >= 0 && GPA <= 4.0)
            this.GPA = GPA;
        else
            System.out.println("Invalid GPA: " + GPA);      
    }

These are the mutators corresponding to each of the three fields. The input for the name and year mutators aren’t checked, but the GPA mutator checks to make sure that the GPA value is in the proper range.

    public String getName() { return name; };
    public int getYear() { return year; };
    public double getGPA() { return GPA; };

    public String toString() {
        return name + "\t" + YEARS[year] + "\t" + GPA;
    }   
}

Finally, these accessors allow the user to find out the name, year, or GPA of a given student. Every class in Java automatically has a toString() method that’s called whenever an object is being printed out directly. We have made this method return the information in Student formatted as a String.

Creating the Student class is only half the battle. We must also create client code to use it.

import java.util.*;

public class StudentRoster {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
		int students = in.nextInt();		(1)
        Student[] roster = new Student[students]; (2)
        for(int i = 0; i < roster.length; i++) { (3)
            in.nextLine();          
            roster[i] = new Student(in.nextLine(), in.nextInt(), in.nextDouble());
        }
        sort(roster); (4)
        for(int i = 0; i < roster.length; i++)
            System.out.println(roster[i]);
    }
1 The main() method in the StudentRoster class begins by reading in the total number of students.
2 Next, it makes an array of type Student of that length.
3 Then, it repeatedly reads in a name, year, and GPA, creates a new Student object with those values, and stores it into the array.
4 After creating all the Student objects, it sorts them with a method call and prints them out.

One oddity in this code is the seemingly superfluous in.nextLine() in the first for loop. This line of code consumes a trailing newline character from previous input. Take it out and see how quickly the program malfunctions.

    public static void sort(Student[] roster) {
        for(int i = 0; i < roster.length - 1; i++) {
            int smallest = i;
            for(int j = i + 1; j < roster.length; j++)
                if(roster[j].getGPA() < roster[smallest].getGPA())
                    smallest = j;
            Student temp = roster[smallest];
            roster[smallest] = roster[i];
            roster[i] = temp;
        }
    }
}

This sort() method is similar to others you’ve seen. It implements selection sort in ascending order based on GPA.

If you run this program, you’ll notice that it doesn’t prompt the user for input. This version of the code is designed for redirected input from a file. A more user friendly, interactive version should prompt the user clearly.

Using OOP is not necessary to solve this problem. Instead of objects, we could have used three separate arrays holding the name, year, and GPA of each student, respectively. However, coordinating these arrays together would become tedious, particularly when sorting.

9.4. Advanced: Nested classes

Inside of a class, you can define fields and methods, but what about other classes? Yes! Doing so creates a nested class. When you define a class inside of an outer class, it can access fields and methods in the outer class, even if they are marked private. Java allows a number of different ways to define a nested class. They’re all useful, but each is subtly different. Some nested classes are tied to a specific object of the outer class while others are not.

9.4.1. Static nested classes

If you mark a nested class with the static keyword, you’re creating a class whose objects are independent of any particular outer class object. Such a class is called a static nested class. Consider the following class definition.

public class Outer {
    private int x;
    private int y;

    public static class Nested {
        private int z;
    }
}

A static nested class is similar to a normal, top-level class with two differences. First, the full name of a nested class is the name of the outer class followed by a dot followed by the nested class name. Second, when given an outer class object, code in a static nested class can access and modify private (and protected) data in the outer class object.

nested
Figure 9.3 A static nested class object is allowed to access data from outer class objects even though there’s no direct relationship between them.

Static nested classes can be used when the class you need is only useful in connection with the outer class. Thus, nesting the class groups it with its outer class. We can create an instance of the nested class above as follows.

Outer.Nested nested = new Outer.Nested();

Because it’s a static nested class, we don’t need an instance of type Outer to create an instance of type Outer.Nested. If you compile Outer.java, it will create two files, Outer.class and Outer$Nested.class. The dollar sign ($) separates the names of each level of nested class in the file name. It’s possible to nest classes inside of nested classes, producing another .class file with another dollar sign and the new class name appended.

Like members, static nested classes can be marked public, private, protected, or package-private (no explicit modifier). These access modifiers control which code can access or instantiate static nested classes using the sames access rules for fields and methods.

Example 9.2 Static nested class for testing

One application for static nested classes is testing. You can write code that tests the functionality of your outer class, fiddling with its fields if needed. Then, because a separate .class file is created, you can deliver only the .class file for the outer class to your customer.

Consider the Square class, similar to the Rectangle class given earlier.

public class Square {
    private int side;

    public Square( int side ) {
        this.side = side;
    }

    public int getArea() {
        return side*side;
    }
}

We could add a static nested class called Test to Square to test that its getArea() and getPerimeter() methods are working properly. The final code might be as follows.

public class Square {
    private int side;

    public Square( int side ) {
        this.side = side;
    }

    public int getArea() {
        return side*side;
    }

    public static class Test {
        public static void main(String[] args) {
            Square square = new Square(5);
            System.out.print("Test 1: ");
            if(square.getArea() == 25)
                System.out.println("Passed");
            else
                System.out.println("Failed");

            square.side = 7;
            System.out.print("Test 2: ");
            if(square.getArea() == 49)
                System.out.println("Passed");
            else
                System.out.println("Failed");
        }
    }
}

To run the tests, you would compile Square.java and then run the nested class by invoking java Square$Test. It’s unwise to use the nested class to change the private fields in square, but we did so to show that it’s allowed in Java. A better test would create a second Square object with a side of length 7.

9.4.2. Inner classes

Another kind of nested class is an inner class. Unlike static nested classes, the objects of inner classes are associated with a particular object of the outer class. You can think of an inner class object living inside an outer class object. It’s impossible to instantiate an inner class object without having an outer class object first. Consider the following class definition.

public class Outer {
    private int a;

    public class Inner {
        private int b;
        private int c;
    }
}

Every instance of Inner must be associated with an instance of Outer. To instantiate an inner class, you use the name of an outer class object, followed by a dot, followed by the new keyword, and then the name of the inner class. We can create an instance of the inner class above as follows.

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

This syntax looks confusing, but it makes inner an object that exists inside of outer. Thus, if there were methods defined in Inner, they could refer to field a, because every instance of Inner would be inside of an instance of Outer with a copy of a.

The relationship between outer and inner objects is one to many. We can instantiate any number of inner class objects that all live inside of the same outer class object.

inner
Figure 9.4 An inner class object is always associated with a specific outer class object.

Another issue with inner classes (as opposed to static nested classes) is that they cannot contain static methods or static fields (except for constants). Since each instance of an inner class is tied to an instance of an outer class, the designers of Java thought that static fields and methods for an inner class really belong in the outer class.

It’s even possible to define a class inside a method, if that class is only referred to in the method. Such a class is called a local class. It’s possible to create an unnamed local class on the fly as well. Such a class is called an anonymous class. Both local and anonymous classes are special kinds of inner classes. Because of the way they’re created and used, we’ll discuss them in Section 10.4

Example 9.3 Inner class objects as iterators

If you create a data structure for other programmers to use, a useful feature is the ability to retrieve each item from the data structure in order. Different threads or methods might need to process these elements independently from each other. Each piece of code can be given an inner class object called an iterator that can repeatedly get the next item in the data structure. Since instances of an inner class can read private data of the outer class, iterators can keep track of where they are inside of the data structure. If outside code were allowed access to the data structure’s internals, it would violate encapsulation. Iterators are a common application of inner classes.

We can create a SafeArray class that only allows data to be written to its internal array if it falls in the legal range of indexes.

public class SafeArray {
    private double[] data;

    public SafeArray(int size) {
        data = new double[size];
    }

    public int set(int index, double value) {
        if(index >= 0 && index < data.length)
            data[index] = value;
    }
}

We could add an inner class called Iterator to SafeArray that allows us to process all the array values without knowing how many there are. This kind of behavior is useful for many dynamic data structures, as discussed in Chapter 18.

public class SafeArray {
    private double[] data;

    public SafeArray(int size) {
        data = new double[size];
    }

    public void set(int index, double value) {
        if( index >= 0 && index < data.length )
            data[index] = value;
    }

    public class Iterator {
        private int index = 0;

        public boolean hasNext() {
            return (index < data.length);
        }

        public double getNext() {
            if(index >= 0 && index < data.length)
                return data[index++];
            else
                return Double.NaN;
        }
    }
}

The following method uses the iterator we’ve defined to find the sum of the values in a SafeArray object.

public static findSum(SafeArray array) {
    double sum = 0;
    SafeArray.Iterator iterator = array.new Iterator();

    while(iterator.hasNext())
        sum += iterator.getNext();

    return sum;
}

9.5. Solution: Nested expressions

We now have enough knowledge to solve the nested expressions problem from the beginning of the chapter. Classes help us divide up the work of solving the problem. We need a stack class that can hold char values. The SymbolStack class allows us to perform the push, pop, and top stack operations with methods of the same names.

Program 9.2 Simple stack class to hold symbols from an input expression.
public class SymbolStack {
    private char[] symbols;
    private int size;
    
    public SymbolStack(int maxSize) { 
        symbols = new char[maxSize]; (1)
        size = 0; (2)
    }
    
    public void push( char symbol ) { symbols[size++] = symbol; } (3)
    public void pop() { size--; } (4)
    public char top() { return symbols[size - 1]; } (5)
    public boolean isEmpty() { return size == 0; } (6)
}
1 Its constructor takes a maximize size for the stack and allocates an array of that size.
2 It also sets the size field to 0 so that we can keep track of how many things are in the stack (and consequently where the top is). All int fields in Java are automatically initialized to 0, but it doesn’t hurt to be explicit.
3 The push() method stores an input char into the stack at location size and then increments size.
4 The pop() method simply decrements size. It has no error checking to prevent a user from popping the stack once it’s already empty.
5 The top() method returns the value at the top of the stack, whose location is size - 1.
6 SymbolStack also defines an isEmpty() method so that we can see if the stack is empty.

Now we need the client code that reads the input and interacts with the stack.

import java.util.*;

public class NestedExpressions {
    public static void main(String[] args) {        
        Scanner in = new Scanner(System.in);
        String input = in.nextLine(); (1)
        SymbolStack stack = new SymbolStack(input.length()); (2)
        char symbol;    
        boolean correct = true; (3)
1 The main() method of this class reads in the input.
2 Then, it creates a SymbolStack called stack with a maximum size of the input length. We know that the stack will never need to hold more than the total input length.
3 It also creates a boolean named correct to keep track of whether or not the input is correctly nested. We start by assuming that it is.
        for(int i = 0; i < input.length() && correct; i++) { (1)
            symbol = input.charAt(i);
            switch(symbol) {
                case '(':
                case '[':
                case '{':
                    stack.push(symbol); (2)
                    break;
                case ')':
				case ']':
				case '}':
                    if(stack.isEmpty() || stack.top() != symbol) (3)
                        correct = false;
                    else
                        stack.pop();
                    break;                
            }
        }
1 This for loop runs through each char in the input.
2 If it’s a left parenthesis, left square bracket, or left curly brace, it pushes the symbol onto the stack.
3 If it’s a right parenthesis, right square bracket, or right curly brace, it checks to see if the stack is empty. Because of short-circuit evaluation, the code doesn’t even look at the top of the stack if it is empty. However, if the stack isn’t empty, it checks to see if the top matches the current symbol. If the stack is empty or its top doesn’t match, correct is set to false. For efficiency, the loop stops early if correct is no longer true.
        if(!stack.isEmpty()) //unmatched left symbols (1)
            correct = false;
        
        if(correct) (2)
            System.out.println("The input is correctly nested!");
        else
            System.out.println("The input is incorrectly nested!");
    }
}
1 After the input has been examined, we check to see if the stack is empty. If it isn’t, there must be some left symbols that weren’t matched with right symbols. In that case, we set correct to false.
2 Finally, we print out whether the input is correctly or incorrectly nested based on the value of correct.

9.6. Concurrency: Objects

Nearly everything in Java is an object: arrays, lists, String values, colors, and even exceptions, which form Java’s error-handling system and are discussed in Chapter 12. Some critics of Java point out that int, double, and the other primitive types are not objects, forcing the programmer to adopt two different programming models. Regardless, threads are stored as objects as well. In Chapter 13, we’ll discuss how to create threads and the various methods that can be used to interact with them.

However, objects of type Thread are not the only ones you deal with when writing concurrent programs. As we’ve just noted, most data in Java is encapsulated in an object. One of the deep reasons for using OOP is safety: We want the private data inside of an object to stay in a consistent state. Due to their inexplicable ability to get out of tight spots, one tradition holds that cats have nine lives. Because of their inquisitive nature, another tradition holds that curiosity killed the cat. Consider the class below that keeps track of the lives a cat has, losing one every time it becomes curious.

Example 9.4 Thread safety

If the relationship between curiosity and mortality is the only feature of a cat you’re trying to model, the class below appears to function well. When the useCuriosity() method is invoked, it removes a life or prints an error message if the cat has already run out of lives. In a single-threaded situation, this object would work perfectly. No cat would be able to lose more than 9 lives.

In a multi-threaded situation, however, there’s no telling when a thread might pause in executing the useCuriosity() method.

Program 9.3 Records the number of lives a cat has left. Each Cat object starts with 9, but loses one each time it uses its curiosity. If no more lives remain, an error message is output.
public class Cat {
    private int lives = 9;
    
    public boolean useCuriosity() {         
        if( lives > 1 ) { (1)
            lives--; (2)
            System.out.println("Down to life " + lives);
            return true;
        }
        else {
            System.out.println("No more lives left!");
            return false;
        }
    }   
}
1 If 100 threads all called useCuriosity(), each one might successfully pass the if on this line before any had decremented lives.
2 Once past the check, nothing would prevent them from continuing on and decrementing lives, resulting in a cat who lost 100 lives, resulting in a total of -91 lives. Such a scenario makes no sense.

In Chapter 14, we’ll discuss how to prevent this problem, using the synchronized keyword to allow only a single thread at a time to execute a section of code. The goal is to make useCuriosity() thread-safe, meaning that its behavior is consistent and correct no matter how many threads try to execute it at the same time.

As you work through this book and begin to write your own concurrent programs, we’ll discuss many ways to make them thread-safe. However, you’re also a consumer of code written by other people. In multi-threaded environments, you might need to use library classes that are thread-safe. For example, AtomicInteger is a thread-safe class designed to store and manipulate int values. In Chapter 18, we’ll talk about the ArrayList and Vector classes, which are both used to hold variable length lists of objects. One of the few differences between them is that ArrayList is not thread-safe while Vector is. There’s even the Collections.synchronizedCollection() method (and other similar methods), which takes a collection that’s not thread-safe and returns a version of it that is.

Java was intended to be multi-threaded from the very beginning, but concurrency was never the most important feature in the language. For that reason, the documentation doesn’t clearly mark which methods are thread-safe. Usually, some of the paragraphs of description above the list of methods say that a class is “synchronized” if it is thread-safe. If it’s not, the documentation may not mention anything. Careful attention is needed to be sure which classes and APIs are thread-safe.

You might wonder why all classes aren’t thread-safe, but everything comes with a price. If a class is thread-safe, its methods are usually marked with the synchronized keyword. The JVM is relatively efficient about how it enforces that keyword, but the computational expense is not zero. Learn the libraries well, and use the right tools for the right job.

9.7. Exercises

Conceptual Problems

  1. Explain the relationship between a class and an object.

  2. What’s the difference between a static method and an instance method?

  3. What’s the purpose of a constructor? Why is it impossible for a constructor to return a value? Why is it impossible for a constructor to be called multiple times on the same object?

  4. A static method can be called directly from a instance method, but an instance method can’t be called directly from a static method. Why?

  5. Describe the uses of accessor and mutator methods. Is it possible to create a method that is both an accessor and a mutator? Why or why not?

  6. Why do we usually mark fields with the private keyword when it would be easier to make all fields public?

  7. What’s the meaning of the this keyword? When is it necessary to use it? When can it be ignored?

  8. Consider the following class definitions.

    public class A {
        private int a;
    
        public int getA() { return a; }
    
        public static void increment() { a++; }
    }
    
    public class B {
        private int b;
    
        public B(int value) {
            b = value;
        }
    
        public A generate() {
            A object = new A();
            object.a = b;
    		return object;
        }
    }

    The field a is used three times in the previous code. Which of these uses cause a compiler error and why?

  9. In Section 9.5, we gave a definition of SymbolStack that implements a simple stack using two fields, as follows.

    private char[] symbols;
    private int size;

    By calling the top() or pop() methods on an empty stack, it’s possible to cause a program to crash. What additional problems could happen if symbols and size were declared public and malicious or poorly written code had access to these fields?

  10. Consider the following class definition.

    public class GroceryItem {
        private String name;
        private double price;
    
        public GroceryItem(String text, double money) {
            String name = text;
            double price = money;
        }
    
        public String getName() { return name; }
        public String getPrice() { return price; }
    }

    This class compiles, but its constructor doesn’t function properly. Why not?

Programming Practice

  1. OOP is often used when the data inside the object must maintain special relationships. Consider a clock with hours, minutes, and seconds. When the number of seconds reaches 60, the number of minutes is increased by 1, and the number of seconds is reset to 0. When the number of minutes reaches 60, the number of hours is increased by 1, and the number of seconds is reset to 0. When the number of hours reaches 13, it’s reset to 1. AM and PM switch whenever the number of hours reaches 12.

    Define a Clock class with private int fields hours, minutes, and seconds and a boolean field PM. Write a constructor that initializes hours to 12, minutes and seconds to 0, and PM to false. Write a mutator increment() that adds 1 to seconds. This mutator should correctly handle all the clock behavior described above. Write an accessor called toString() that returns a nicely formatted version of the time as a String. For example, the initial time would be returned as "12:00:00 AM". Make sure you pad the output for seconds and minutes with an extra "0" if they’re less than 10.

  2. Draw on any of your hobbies to come up with a collection of items, whether those items are books you like to read, athletes you follow, music you collect, or anything else that’s easy to classify. Then, create a class that can describe one of these items with three to five attributes. For example, the important attributes of a book might be author, title, genre, and page count. Each of these attributes should be stored as a private field and manipulated with public accessor and mutator methods.

    Using an array, create a database of these objects. Write methods that print out all objects that have a particular value for an attribute. For example, a book database program should let the user input that he or she is looking for all books whose author is "Alexandre Dumas". You might wish to use input redirection so that you don’t have to enter data about your objects repetitively.

  3. The java.awt package defines a class called Point that can be used to manipulate an (x, y) pair in programs involving the Cartesian coordinate system. Create your own Point class with int values x and y as fields.

    Create one constructor that allows the user to specify values for x and y and a default constructor that takes no arguments and sets both x and y to 0. Create accessors and mutators for x and y.

    Finally, create a method with the signature public double distance(Point p) that finds the distance between the current Point object and the Point object p passed in as an argument. Recall that the following formula finds the distance d between two 2D points.

    distance

    Write client code that allows you to create two Point objects and test if the distance() method gives the right answer.

  4. Re-implement the solution from Section 9.5 so that it performs its input and output with GUIs created using JOptionPane.

Experiments

  1. Objects are great tools for solving problems, but there’s some additional overhead associated with creating objects and calling methods.

    Write a piece of code that allocates an array of 10,000,000 int values. Iterate through that array, storing the value i into index i, and time the process using an OS time command. As you know, the Integer wrapper class allows us to store an int value in object form. Repeat the experiment, but instead of int values, allocate an array to hold 10,000,000 Integer objects. Iterate through the array again, storing an Integer object into each index of the array. For index i, store a new Integer objected created by passing value i into its constructor. Compare the time taken to the previous time for int values. Do you think this is a reasonable way to estimate the time it takes to call a constructor and allocate a new object?

10. Interfaces

Our work is the presentation of our capabilities.

— Edward Gibbon

10.1. Problem: Sort it out

Learning to use classes as we did in Chapter 9 provides us with useful tools. First, we can group related data together. Then, we can encapsulate that data so that it can only be changed in carefully controlled ways. But these ideas are only a fraction of the true power of object orientation available when we exploit relationships between different classes. Eventually, in Chapter 11, we’ll talk about hierarchical relationships, where one class can be the parent of many other child classes.

However, there’s a simpler relationship that classes can share, the ability to do some specific action or answer some specific question. If we know that several different classes can all do the same things, we want to be able to treat them in a unified way. For example, imagine that you’re writing code to do laboratory analysis of the free radical content of a number of creatures and items. For this analysis it’s necessary to sort the test subjects by weight and by age.

The subjects you’re going to sort will be objects of type Dog, Cat, Person, and Cheese. All of these subjects will have some calendar age. Naturally, the age we’re interested for a Dog is 7 times its calendar age. At the same time, because a cat has nine lives, we’ll use 1/9 of its calendar age. The age we use for Person and Cheese objects is simply their calendar age.

As we discussed in Example 6.2, sorting is one of the most fundamental tools for computer scientists. In that example, we introduced selection sort. Here we’re going to use another sort called bubble sort. We introduce bubble sort to expose you to another way to sort data. Although both are easy to understand, neither bubble sort nor selection sort are the fastest ways known to sort lists.

Bubble sort works by scanning through a list of items, a pair at a time, and swapping the elements of the pair if they’re out of order. One scan through the list is called a pass. The algorithm keeps making passes until no consecutive pair of elements is out of order. Here’s an implementation of bubble sort that sorts an array of int values in ascending order.

boolean swapped = true;
int temp;
while(swapped) {
    swapped = false;
    for(int i = 0; i < array.length - 1; i++)
        if(array[i] > array[i + 1]) {
            temp = array[i];
            array[i] = array[i + 1];
            array[i + 1] = temp;
            swapped = true;
        }
}
Pitfall: Array out of bounds

Note that index variable i stops before array.length - 1 since we don’t want to get an ArrayIndexOutOfBoundsException when looking at array[i + 1]. Another way to think about it is that we only want to loop array.length - 1 times, since that’s how many neighboring pairs of values there are in array.

The only change needed to make a bubble sort work with lists of arbitrary objects (instead of just int values) is the if statement. Likewise, changing the greater than (>) to a less than (<) will change the sort from ascending to descending.

10.2. Concepts: Making a promise

Knowing how to sort a list of items is half the problem we want to solve. The other half is designing the classes so that sorting by either age or weight is easy. And easy sorting isn’t enough: We want to follow good object-oriented design so that sorting future classes will be easy as well.

For these kinds of situations, Java has a feature called interfaces. An interface is a set of methods that an object must have in order to implement that interface. If an object implements an interface, it’s making a promise to have a version of each of the methods listed in the interface. It can choose to do anything it wants inside the body of each method, but it must have them to compile. Interfaces are also described as contracts, since a contract is a promise to do certain things. One reason that interfaces are so useful is because Java allows objects to implement any number of them.

The purpose of an interface is to guarantee that an object has the capability to do something. In the case of this sorting problem, we’ll need to sort by age and weight. Consequently, the ability to report age and weight are the two important capabilities that the objects must have.

10.3. Syntax: Interfaces

Declaring an interface is similar to declaring a class except that interfaces can only hold abstract methods, default methods, static methods, and constants. Note that interfaces can never contain fields other than constants. Because the purpose of an interface is to ensure that an object has certain publicly available capabilities, all methods and constants listed in an interface are assumed to be public. It’s legal but clutters code to mark interface members with public, and it’s illegal to mark them with private or protected.

An abstract method is one that doesn’t have a body. Its return type, name, and parameters are given, but this information is concluded with a semicolon (;) instead of a method body.

Here’s an interface for a guitar player that defines two abstract methods.

public interface Guitarist {
    void strumChord(Chord chord);
    void playMelody(Melody notes);
}

Any object that implements this interface must have both of these methods, declared with the access modifier public, the same return types, and the same parameter types. We could create a RockGuitarist class that implements Guitarist as follows.

public class RockGuitarist implements Guitarist {
    public void strumChord(Chord chord) {
        System.out.print("Totally wails on that " + chord.getName() + " chord!");
    }
    
    public void playMelody(Melody notes) {
        System.out.print("Burns through the notes " + notes.toString() +
			" like Jimmy Page!");
    }
}

Or we can create a ClassicalGuitarist class. A classical guitarist is going to approach playing a chord or a melody differently from a rock guitarist. Java only requires that all the methods in the interface are implemented.

public class ClassicalGuitarist implements Guitarist {
    public void strumChord(Chord chord) {
        System.out.print("Delicately voices a " + chord.getName() + " chord.");      
    }
    
    public void playMelody(Melody notes) {
        System.out.print("Plucks the melodic line " + notes.toString() +
			" with the skill of John Williams.");     
    }
}

Interfaces make our code more flexible because we can use a reference with an interface type to refer to any objects that implement the interface. In the following snippet of code, we do this by creating 100 Guitarist references, each of which randomly points to either a RockGuitarist or a ClassicalGuitarist.

Guitarist[] guitarists = new Guitarist[100];
Random random = new Random();
for(int i = 0; i < guitarists.length; i++)
    if(random.nextBoolean())
        guitarists[i] = new RockGuitarist();
    else
        guitarists[i] = new ClassicalGuitarist();

One benefit of using interfaces is that more mistakes can be caught at compile time instead of run time. If you implement an interface with a class you’re writing but forget (or misspell) a required method, the compiler will fail to compile that class. If you remember all of the required methods but forget to specify that your class implements a particular interface, the compiler will fail when you try to use that class in a place where that interface is required.

If you have code that takes some arbitrary object as a parameter, you can determine if that object implements a particular interface by using the instanceof keyword. For example, the following method takes a Chord object and an object of type Object. The Object type is the most basic reference type there is. All reference types in Java can be treated as an Object type. Thus, a reference of type Object could point at any object of any type. In this case, if the Object turns out to implement the Guitarist interface, we can cast it to a Guitarist and use it to strum a chord.

void checkBeforeStrum(Object unknown, Chord chord) {
    if(unknown instanceof Guitarist) {
        Guitarist player = (Guitarist)unknown;
        player.strumChord(chord);
    }
    else
        System.out.println("That's not a guitarist!");
}
Pitfall: Interfaces cannot be instantiated

It’s tempting to try to create an object of an interface type. If you think about doing so carefully, it should be clear why this is impossible. An interface is only a list of promises, and it’s not a class, a template for an object. It has neither data nor methods to manipulate data. Thus, the following code will fail to compile.

Guitarist guitarist = new Guitarist();

As we showed before, we are permitted to make a reference to an object that implements Guitarist or make an array that holds such references. The interface itself, however, can never be instantiated.

10.3.1. Default methods

Before Java 8, only abstract methods (methods without bodies) could be added to interfaces. And that made sense: Interfaces were promises to do things, while classes actually did those things. Unfortunately, a problem became apparent over time. Some Java library designers would create a useful interface, and many people would write classes that implement that interface. Eventually, someone would want to add a new method to the original interface, but doing so would break lots of code. All the existing classes that implemented the interface would fail to compile until they added the new method.

To allow new methods to be added to old interfaces without breaking existing classes, the idea of a default method was added to the rules for Java interfaces. A method with a body could be added to an interface, provided that it was marked with the default keyword. If a class implements that interface and has a public method that matches the default method, the class’s method will be used. However, if a class doesn’t have a matching method, the default method will be used, and the class will still compile.

Consider the following interface similar to the original Guitarist interface except that it contains a default tune() method.

public interface DefaultGuitarist {    
    void strumChord(Chord chord);
    void playMelody(Melody notes);
	
	default boolean tune() {
		System.out.println("Tuning guitar...");
		return true;
	}
}

If a class implements DefaultGuitarist and includes its own tune() method, that method will satisfy the interface. On the other hand, a class that implements DefaultGuitarist without a tune() method will automatically include the tune() method given above, which prints a message about tuning and then returns true (presumably indicating that tuning was successful). The way that a class can supply a method that overrides a default method is very similar to the way that methods can be overridden with inheritance, as discussed in Section 11.3.4. Note that a default method must have a body.

An additional issue can arise with default methods when a class implements two or more interfaces that specify default methods with the same signature. Consider the following interface that also has a tune() method.

public interface DefaultEngine {    
    void start();
    int getRedline();
	
	default boolean tune() {
		System.out.println("Checking spark plugs...");
		return true;
	}
}

By itself, this interface doesn’t cause any problems. However, when a class implements both it and DefaultGuitarist, there can be a conflict.

public class EngineGuitarist implements DefaultGuitarist, DefaultEngine {
	public void strumChord(Chord chord) {
		System.out.println("Rumbles out " + chord.getName() + "!");
	}
	
	public void playMelody(Melody notes) {
		System.out.println("Plays " + notes.toString() + " revving at different RPMs!");
	}
	
	public void start() {
		System.out.println("*Crank*, *crank*, *vroooom!*");
	}
	
    public int getRedline() {
		return 7000;
	}
}

This strange class is the template for an object that’s simultaneously an engine and a guitarist, implementing both DefaultGuitarist and DefaultEngine. It has public methods for strumChord() and playMelody(), satisfying the DefaultGuitarist interface. It also has public methods for start() and getRedline(), satisfying the DefaultEngine interface. But it’s received two different default methods for tune(), one that tunes like a guitarist and one that tunes like an engine. As a consequence, an EngineGuitarist object wouldn’t know which tune() to call and won’t compile as it’s written.

You’re allowed to create classes like EngineGuitarist that implement more than one interface with the same default method, but you need to create a tune() method inside EngineGuitarist to make clear what happens when tune() is called.

The use of default methods in interfaces is relatively rare, and situations where there are two conflicting default methods are rarer still. Even so, it’s important to understand how all the features of a language work. Note that it’s unlikely that you’ll be writing interfaces with default methods often, since their primary use is to add capabilities to existing interfaces without breaking classes that already use those interfaces.

10.3.2. Interfaces and static

When designing an interface, you can’t mark an abstract method static. One way to think about this rule is that interfaces specify actions (in the form of methods) that an object can do. Interfaces don’t specify requirements for a class, such as its static methods or its class variables. To illustrate, even with Chord and Melody defined, the following interface fails to compile.

public interface AbstractStaticGuitarist {
    void strumChord(Chord chord);
    void playMelody(Melody notes);
    static int getStrings();
}

However, in Java 8 and higher, non-abstract static methods are allowed in interfaces. Such methods aren’t promises that objects of a class must fulfill. Instead, they’re simply utility methods that perform some task associated with the interface.

public interface StaticGuitarist {
    void strumChord(Chord chord);
    void playMelody(Melody notes);
    
	static String nextNote(String note) {
		char letter = note.charAt(0);
		if(note.length() == 2) {
			if(note.charAt(1) == 'b')
				return "" + letter;
			else {
				switch(letter) {
				case 'B':
				case 'E':
					return letter + "#";
				case 'G':
					return "A";
				default:
					return "" + (letter + 1);
				}
			}
		}
		else {
			switch(letter) {
			case 'B':
			case 'E':
				return "" + (letter + 1);
			default:
				return letter + "#";
			}
		}	
	}
}

This interface includes the method nextNote(), which takes a String representation of a note such as "D#" or "Bb" and returns a String with a representation of a note one half step higher. The rules for musical notes are somewhat complex, so this method is useful even when separate from any particular class that implements StaticGuitarist. The nextNote() method would be called just like any static method, using the name of its container StaticGuitarist followed by a dot followed by the name of the method itself.

String note = StaticGuitarist.nextNote("F");

Neither fields nor class variables are allowed in interfaces, so neither static nor default methods will ever change any data inside the interface. This is the fundamental difference between an interface and a class: Interfaces never contain state. Even so, class constants are allowed. Thus, we could define static final values that might be useful to any class implementing an interface. With Chord and Melody defined, the following interface will compile.

public interface ConstantGuitarist {    
    static final int MAJOR = 1;
    static final int NATURAL_MINOR = 2;
    static final int HARMONIC_MINOR = 3;
    static final int MELODIC_MINOR = 4;
    static final int CHROMATIC = 5;
    static final int PENTATONIC = 6;    

    void strumChord(Chord chord);
    void playMelody(Melody notes);    
}

Another source of confusion is that class variables will be both static and final by default in an interface, even if those keywords aren’t used.

Some Java users object to the use of constants in interfaces, since the purpose of an interface is to define a list of a requirements for objects of a class rather than dealing with data values. Nevertheless, constants are allowed in interfaces, and the Java API uses them in many cases.

Example 10.1 Supermarket pricing

Interface names often include the suffix -able, for example, Runnable, Callable, and Comparable. This suffix is typical because it reminds us that a class implementing an interface has some specific ability. Let’s consider an example in a supermarket in which the items could have very little in common with each other but they all have a price. We could define the interface Priceable as follows.

interface Priceable {
    double getPrice();
}

If bananas cost $0.49 a pound, we can define the Bananas class as follows.

public class Bananas implements Priceable {
    public static final double PRICE_PER_POUND = 0.49;
    private double weight;
    
    public Bananas(double weight) { this.weight = weight; }

    public double getPrice() {
        return weight*PRICE_PER_POUND;
    }
}

If eggs are $1.50 for a dozen large eggs and $1.75 for a dozen extra large eggs, we can define the Eggs class as follows.

public class Eggs implements Priceable {
    public static final double PRICE_PER_DOZEN_LARGE = 1.5;
    public static final double PRICE_PER_DOZEN_EXTRA_LARGE = 1.75;
    private int dozens;
    private boolean extraLarge;
    
    public Eggs(int dozens, boolean extraLarge) {
        this.dozens = dozens;
        this.extraLarge = extraLarge;
    }       

    public double getPrice() {
        if(extraLarge)
            return dozens*PRICE_PER_DOZEN_EXTRA_LARGE;
        else
            return dozens*PRICE_PER_DOZEN_LARGE;            
    }
}

Finally, if water is $0.99 a gallon, we can define the Water class as follows.

public class Water implements Priceable {
    public static final double PRICE_PER_GALLON = 0.99; 
    private int gallons;    

    public Water(int gallons) { this.gallons = gallons; }     

    public double getPrice() { return gallons*PRICE_PER_GALLON; }
}

Each class could be much more complicated, but the code shown is all that’s needed to implement the Priceable interface. Even though there’s no clear relationship between bananas, eggs, and water, a shopping cart filled with these items (and any others implementing the Priceable interface) could easily be totaled at the register. If we represent the shopping cart as an array of Priceable items, we could write a simple method to total the values like so.

public static double getTotal(Priceable[] cart) {
    double total = 0.0;
    for(int i = 0; i < cart.length; i++)
        total += cart[i].getPrice();

    return total;
}

Note that we can pass in Bananas, Eggs, Water, and many other kinds of objects in a Priceable array as long as they all implement this interface. Even though it’s impossible to create an object with an interface type, we can make as many references to it as we want.

10.4. Advanced: Local and anonymous classes

If you haven’t read Section 9.4, you may want to look over that material to be sure you understand what nested classes and inner classes are. Recall that a normal inner class is declared inside of another class, but it’s also legal to declare a class inside of a method. Such a class is called a local class. Under some circumstances, it’s useful to create an inner class with no name, called an anonymous class.

Both kinds of classes are inner classes. They can access fields and methods, even private ones. Like other inner classes, they’re not allowed to declare static variables other than constants. We bring up these special kinds of classes in this chapter because they’re commonly used to create a class with a narrow purpose that implements a required interface.

10.4.1. Local classes

A local class declaration looks like any other class declaration except that it occurs within a method. The name of a local class only has meaning inside the method where it’s defined. Because the scope of the name is only the method, a local class cannot have access modifiers such as public, private, or protected applied to it.

Consider the following method in which an Ellipse class is defined locally. Recall that an ellipse (or oval) has a major (long) axis and a minor (short) axis. The area of an ellipse is half its major axis times half its minor axis times π. (Because the major and minor axes of a circle are its diameter, this formula becomes πr2 in that case.)

ellipse
Figure 10.1 Area of an ellipse.
public static void createEllipse(double a1, double a2) {
    class Ellipse {
        private double axis1;
        private double axis2;

        public Ellipse(double axis1, double axis2) {
            this.axis2 = axis2;
            this.axis1 = axis1;
        }

        public double getArea() {
            return Math.PI*0.5*axis1*0.5*axis2;
        }
    }

	Ellipse e = new Ellipse(a1, a2);
    System.out.println("The ellipse has area " + e.getArea());
}

This Ellipse class cannot be referred to by any other methods. Since an Ellipse class might be useful in other code, a top-level class would make more sense than this local class. For that reason, local classes are not commonly used.

However, we can make local classes more useful if they implement interfaces. Consider the following interface which can be implemented by any shape that returns its area.

public interface AreaGettable {
    double getArea();
}

The method below takes an array of AreaGettable objects and sums their areas.

public static double sumAreas(AreaGettable[] shapes) {
    double sum  = 0.0;
    for(int i = 0; i < shapes.length; i++)
        sum += shapes[i].getArea();

    return sum;
}

If we create a local class that implements AreaGettable, we can use it in conjunction with the sumAreas() method. In the following method, we expand the local Ellipse class in this way and fill an array with 100 Ellipse instances, which can then be passed to sumAreas().

public static void createEllipses() {
    class Ellipse implements AreaGettable {
        private double axis1;
        private double axis2;

        public Ellipse(double axis1, double axis2) {
            this.axis2 = axis2;
			this.axis1 = axis1;
        }

        public double getArea() {
            return Math.PI*0.5*axis1*0.5*axis2;
        }
    }

    AreaGettable[] ellipses = new AreaGettable[100];
    for(int i = 0; i < ellipses.length; i++)
        ellipses[i] = new Ellipse(Math.random() * 25.0, Math.random() * 25.0);
    double sum = sumAreas(ellipses);

    System.out.println("The total area is " + sum);
}

Even though the Ellipse class had the getArea() method before, the compiler wouldn’t have allowed us to store Ellipse references in an AreaGettable array until we marked the Ellipse class as implementing AreaGettable. As in Example 10.1, we used an array with an interface type.

10.4.2. Anonymous classes

This second Ellipse class is more useful since objects with its type can be passed to other methods as an AreaGettable reference, but declaring the class locally provides few benefits over a top-level class. Indeed, local classes are seldom preferable to top-level classes. Although anonymous classes behave like local classes, they can be conveniently created at any point.

An anonymous class has no name. It’s created on the fly from some interface or parent class and can be stored into a reference with that type. In the following example, we modify the createEllipses() method so that it creates an anonymous class which behaves exactly like the Ellipse class and implements the AreaGettable interface.

public static void createEllipses() {
    AreaGettable[] ellipses = new AreaGettable[100];

    for(int i = 0; i < 100; i++) {
        final double value1 = Math.random() * 25.0;
        final double value2 = Math.random() * 25.0;

        ellipses[i] = new AreaGettable() {
            private double axis1 = value1;
            private double axis2 = value2;

            public double getArea() {
                return Math.PI*0.5*axis1*0.5*axis2;
            }
        };
    }

    double sum = sumAreas(ellipses);
    System.out.println("The total area is " + sum);
}

The syntax for creating an anonymous class is ugly. First, you use the new keyword followed by the name of the interface or parent class you want to create the anonymous class from. Next, you put the arguments to the parent class constructor inside of parentheses or leave empty parentheses for an interface. Finally, you open a set of braces and fill in the body for your anonymous class. When defining an anonymous class, the entire body is crammed into a single statement, and you will often need to complete that statement with a semicolon (;).

Anonymous classes don’t have constructors. If you need a constructor, you will have to create a local class. Constructors usually aren’t necessary since both local and anonymous classes can see local variables and fields and use those to initialize values. Although any fields can be used, local variables must be marked final (as shown above) if their values will be used by local or anonymous classes. This restriction prevents local variables from being changed in unpredictable ways by methods in the local class.

It might not be easy to see why anonymous classes are useful. Both the Java API and libraries written by other programmers have many methods that require parameters whose type implements a particular interface. Without anonymous classes, you’d have to define a whole named class and instantiate it just for that method, even if you never use it again.

Using anonymous classes, you can create such an object in one step, right where you need it. This practice is commonly used for creating listeners for GUIs. A listener is an object that does the right action when a particular event happens. If you need many different listeners in one program, it can be convenient to create anonymous classes that can handle each event rather than defining many named classes which each have a single, narrow purpose. We’ll use this technique in Chapter 15.

10.5. Solution: Sort it out

It’s not difficult to move from totaling the value of items as we did in Example 10.1 to sorting them. Refer to the following class diagram as we explain our solution to the sorting problem posed at the beginning of the chapter. Dotted lines are used to show the “implements” relationship.

ageandweight
Figure 10.2 Sorting relationships.

We’ll start with the definitions of the two interfaces we’ll use to compare objects.

public interface Ageable {
    int getAge();
}
public interface Weighable {
    double getWeight();
}

Classes implementing these two interfaces will be able to give their age and weight independently. The next step is to create the Dog, Cat, Person, and Cheese classes which implement them.

We’ll see in Chapter 11 that the Dog, Cat, and Person classes could inherit from a common ancestor (such as Creature or Mammal) which implements the Ageable and Weighable interfaces. That design could reduce the total amount of code needed. For now, each class will have to implement both interfaces directly.

public class Dog implements Ageable, Weighable {
    private int age;
    private double weight;  
    
    public Dog(int age, double weight) {
        this.age = age;
        this.weight = weight;
    }
    
    public int getAge() { return age*7; }
    
    public double getWeight() { return weight; }
}
public class Cat implements Ageable, Weighable {
    private int age;
    private double weight;  
    
    public Cat(int age, double weight) {
        this.age = age;
        this.weight = weight;
    }
    
    public int getAge() { return age/9; }
    
    public double getWeight() { return weight; }   
}
public class Person implements Ageable, Weighable {
    private int age;
    private double weight;  
    
    public Person(int age, double weight) {
        this.age = age;
        this.weight = weight;
    }
    
    public int getAge() { return age; }
    
    public double getWeight() { return weight; }   
}
public class Cheese implements Ageable, Weighable {
    private int age;
    private double weight;
    private String type;
    
    public Cheese(int age, double weight, String type) {
        this.age = age;
        this.weight = weight;
        this.type = type;
    }
    
    public int getAge() { return age; }
    
    public double getWeight() { return weight; }
    
    public String getType() { return type; }
}

With the classes in place, we can assume that client code will instantiate some objects and perform operations on them. All that’s necessary is to write the method that will do the sorting. We can wrap the bubble sort code given earlier in a method body with only a few changes to generalize the sort beyond int values.

public void sort(Object[] array, boolean age) {
    boolean swapped = true;
    Object temp;
    while(swapped) {
        swapped = false;
        for(int i = 0; i < array.length - 1; i++)
            if(outOfOrder(array[i], array[i + 1], age) {
                temp = array[i];
                array[i] = array[i + 1];
                array[i + 1] = temp;
                swapped = true;
            }
    }
}

In this method, the boolean age is true if we’re sorting by age and false if we’re sorting by weight. Note that the array elements and temp have the Object type. Recall that any object can be stored in a reference of type Object.

The only other change we needed was to replace the greater-than comparison (>) with the outOfOrder() method, which we define below.

public boolean outOfOrder(Object o1, Object o2, boolean age) {
    if(age) {
        Ageable age1 = (Ageable)o1;
        Ageable age2 = (Ageable)o2;
        return age1.getAge() > age2.getAge();
    }
    else {
        Weighable weight1 = (Weighable)o1;
        Weighable weight2 = (Weighable)o2;
        return weight1.getWeight() > weight2.getWeight();
    }
}

Even though we’ve designed our program for objects that implement both the Ageable and Weighable interfaces, the compiler only sees Object references in the array. Thus, we must cast each object to the appropriate interface type to do the comparison. There’s a danger that a user will pass in an array with objects which do not implement both Ageable and Weighable, causing a ClassCastException. To allow for universal sorting methods, the Java API defines a Comparable interface which can be implemented by any class which requires sorting. With Java 5 and higher, the Comparable interface uses generics to be more type-safe, but we won’t discuss how to use this interface until we cover generics in Chapter 18.

10.6. Concurrency: Interfaces

As we discussed in Section 10.2, implementing an interface means promising to have public methods with the signatures specified in the interface definition. Making a promise seems only tangentially related to having multiple threads of execution. Indeed, interfaces and concurrency do not overlap a great deal, but there are two important areas where they affect one another.

The first is that a special interface called the Runnable interface can be used to create new threads of execution. Runnable is a very simple interface, containing the single signature void run(). Essentially, any object with a run() method that takes no arguments and returns no values can be used to create a thread of execution. Just as a regular program has a single starting place, the main() method, some method needs to be marked as a starting place for additional threads. For more information about using the Runnable interface, refer to Section 13.4.5.

The second connection between interfaces and concurrency is more philosophical. What can you specify in an interface? The rules for interfaces in Java are relatively limited: You can require a class to have a public instance method with specific parameters and a specific return type. Java interfaces don’t allow you to require a static method.

In Chapter 14, we will discuss a key way to make classes thread-safe by using the synchronized keyword. Like static, Java does not allow an interface to specify whether a method is synchronized. Thus, it’s impossible to use an interface to guarantee that a method will be thread-safe.

As with all interface usage, this restriction cuts both ways: If you’re designing an interface, there’s no way to guarantee that implementing classes use synchronized methods. On the other hand, if you’re implementing an interface, the designer may hope that your class uses synchronized (or otherwise thread-safe) methods, but the interface cannot force you to do so. Whenever thread-safety is an issue, make sure you read (or write) the documentation carefully. Since there’s no way to force programmers to use the synchronized keyword, the documentation may be the only guide.

10.7. Exercises

Conceptual Problems

  1. What’s the purpose of an interface?

  2. Why implement an interface when it puts additional requirements on a class yet adds no functionality?

  3. Is it legal to have methods marked private or protected in an interface? Why did the designers of Java make this choice?

  4. What’s the instanceof keyword used for? Why is it useful in the context of interfaces?

  5. What kind of programming error causes a ClassCastException?

  6. Create an interface called ColorWavelengths that only contains constants storing the wavelengths in nanometers for each of the seven colors of light, as given below.

    Color Wavelength (nm)

    Red

    680

    Orange

    605

    Yellow

    580

    Green

    545

    Blue

    473

    Indigo

    430

    Violet

    415

  7. Write an interface called Clock that specifies the functionality a clock should have. Remember that the classes that implement the clock may tell time in different ways (hourglass, water clock, mechanical movement, atomic clock), but they must share the basic functionality you specify.

  8. There are four compiler errors in the following interface. Name each one and explain why it’s an error.

    public interface Singable {
        public int SOPRANO = 1;
        public static int ALTO = 2;
    
        public void sing();
        private String chant();
        public boolean hasDeepVoice() {
            return false;
        }
        public static boolean hasPerfectPitch();
        public synchronized void tune(int frequency);
    }
  9. Consider the interface defined below.

    public interface Explodable {
        boolean explode(double megatons);
    }

    Which of the following classes properly implement Explodable?

    public class Dynamite implements Explodable {
        public boolean explode() {
            System.out.println("BOOM!");
            return true;
        }
    }
    
    public class AtomicBomb implements Explodable {
        public boolean explode(double size) {
            System.out.println("A huge " + size + " megaton blast shakes the earth!");
            return true;
        }
    }
    
    public class Grenade {
        public boolean explode(double megatons) {
            return true;
        }
    }
    
    public class Firecracker implements Explodable {
        private boolean explode(double megatons) {
            return (megatons < 0.0000001);
        }
    }
  10. Write a single class that correctly implements the following three interfaces.

    public interface Laughable {
        boolean laugh(int times);
    }
    public interface Cryable {
        void cry(int tears, boolean moaning);
    }
    public interface Shoutable {
        void shout(double volume, String words);
    }
  11. If you’re sorting a list of items n elements long using bubble sort, what’s the minimum number of passes you’d need to be sure the list is sorted, assuming the worst possible ordering of items to start with? (Hint: Imagine the list is in backward order.) What’s the minimum number of passes if the list is already sorted?

Programming Practice

  1. Add client code that randomly creates the objects needing sorting in the solution from Section 10.5. Design and include additional classes Wine and Tortoise that both implement Ageable and Weighable. Add toString() methods to each class so that their contents can be easily output. Make sure you print out the list of objects after sorting to test your implementation.

  2. Refer to the sort given as a solution in Section 10.5. Add another boolean to the parameters of the sort which specifies whether the sort is ascending or descending. Make the needed changes throughout the code to add this functionality.

  3. After learning about threads in Chapter 13, refer to the simple bubble sort from Section 10.1. The goal is now to parallelize the sort. Write some code which will generate an array of random int values. Design your code so that you can spawn n threads. Partition the single array into n arrays and map one partition to each thread. Use your bubble sort implementation to sort each partition. Finally, merge the arrays back together, in sorted order, into one final array. For now, use just one thread (ideally the main thread) to do the merge.

    The merge operation is a simple idea, but it’s easy to make mistakes in its implementation. The idea is to have three indexes, one for each of the two arrays you’re merging and one for the result array. Always take the smaller (or larger, if sorting in descending order) element value from the two arrays and put it in the result. Then increment the index from the array you took the data from as well as the index of the result array. Be careful not to go beyond the end of the arrays which are being merged. An implementation of merging can be found in Example 19.8.

Experiments

  1. Once you have implemented the sort in parallel from Exercise 10.14, time it against the sequential version. Try two, four, and eight different threads. Be sure to create one random array and use copies of the original array for both the parallel and sequential versions. Be careful not to sort an array that’s already sorted! Try array sizes of 1,000, 100,000, and 1,000,000. Did the performance increase? Was it as much as you expected?

11. Inheritance

Your children are not your children.
They are the sons and daughters of Life’s longing for itself.
They come through you but not from you,
And though they are with you yet they belong not to you.

— Khalil Gibran

11.1. Problem: Boolean circuits

In Chapter 4, we talked extensively about Boolean algebra and how it can be applied to if statements in order to control the flow of execution in your program. The commands that we give in software must be executed by hardware in order to have an effect. It shouldn’t be surprising that computer hardware is built out of digital circuits that behave according to the same rules as Boolean logic. Each component of these circuits is called a logic gate. There are logic gates corresponding to all the Boolean operations you’re used to: AND, OR, XOR, NOT, and others.

The output of an AND gate is the result of performing a logical AND on its inputs. That is, its output is true if and only if both of its inputs are true. The same correlation exists between each gate and the Boolean operator with the same name. At the level of circuitry, a 1 (or “on”) is often used to represent true, and a 0 (or “off”) is often used to represent false. Modern computer circuitry is built almost entirely out of such gates, performing addition, subtraction, and all other basic operations as complicated combinations of logic gates, where each digit of every number is a 1 or 0 determined by the circuit.

Because these circuits can become large and unwieldy, your problem is to write a Java program that will allow a user to specify the design of such a circuit and then see what its output is. The input to this program will be a text file redirected to standard input that gives the number of gates of the circuit, lists what each gate is, and then lists the connections between them.

The following is an example of input to make a circuit with six components.

6
true
false
AND
XOR
NOT
OUTPUT 1
2 0 1
3 2 1
4 3
5 4

The first line specifies the total number of components. The next two components give either true or false inputs, depending on their names. The AND and the XOR correspond to gates of the same name which each take two inputs. The NOT corresponds to a NOT gate with a single input. Finally, OUTPUT 1 is the single output of this circuit that we’re interested in. After the list of gates is a list of how they’re connected. The line 2 0 1 specifies that the gate at index 2, which happens to be an AND gate, has an input from the gate at index 0 and an input from the gate at index 1. In other words, the AND gate has one true input and one false input. The final circuit produced would look like the following.

circuit
Figure 11.1 Final circuit diagram showing the six components.

We’re only interested in the value of any OUTPUT gates. Thus, the program that’s simulating this circuit would print the following.

OUTPUT 1: true

There are many different ways to implement a solution to this problem. The total number of gates is given as the first input. Thus, you can make an array of gates. When you go to connect them, the indexes given in the input will map naturally onto the gates in the array. But what type should the array be? You could create a Gate class that could do the work of any conceivable gate, but the implementation would be awkward. All gates would have to have two inputs even if they don’t need any. Adding different kinds of gates later (like a NAND, for example) would mean rewriting the Gate class.

Instead, a cleaner approach to the solution is to use inheritance. In object-oriented languages like Java, inheritance is a process that allows you to create a new, specialized class from a more general, preexisting class.

We recommend the following inheritance hierarchy, in which the arrows point from each child class to its parent class.

gates
Figure 11.2 Recommended inheritance hierarchy.

As you can see, every class inherits from the Gate class. If your array can be of type Gate, it will be able to hold objects of any child of Gate. UnaryOperator, BinaryOperator, True, and False are children of the basic Gate class. Then, Output and Not are children of UnaryOperator since each only has a single input. Naturally, And, Or, and Xor are children of BinaryOperator, since they all have two inputs.

If this jumble of classes seems bewildering, don’t be discouraged. Each is very short and easy to write. We’ll explain what the inheritance relationship means and how to use it in the next few sections.

11.2. Concepts: Refining classes

Here we give a brief overview of inheritance that will give us enough information to continue onward. We’ll cover some of the deeper areas of the subject in Chapter 17.

11.2.1. Basic inheritance

The process of creating an inherited class out of an existing class is called inheriting a class, deriving a class, or simply subclassing. The class that already exists is called a parent class, base class, or superclass and the new class is called a child class, derived class, or subclass.

When you create a child class from a parent class, the child class inherits all of its fields and methods. Thus, you can use a child class object anywhere you’d use the parent class object. This relationship explains the terms superclass and subclass: Since you can treat any subclass object as if it were a superclass object, the superclass type can be thought of as a superset including all subclass objects.

The names superclass and subclass can sound misleading because the subclass can usually do more than the superclass. From that perspective, the child class has a superset of the fields and methods of its parent class. To avoid confusion, we favor the terminology of parent and child classes.

11.2.2. Adding functionality

When creating a child class, a programmer will normally add functionality above and beyond the original parent class. Otherwise, there’s little point in creating a child class. For example, a simple Fish class might be able to do things like swim and feed. A child Flounder class has the additional ability to camouflage itself. Another child class, the Shark class, adds the ability to eat other Fish objects.

By adding a few methods, we can create a new class with special abilities without interfering with the basic functionality of the underlying class.

11.2.3. Code reuse

Of course, a programmer who wished to program a Shark class could simply copy and paste in all the code from the Fish class and then make the necessary additions. In many ways the evolution of modern programming languages has been to reduce the need for copying and pasting.

Old mistakes are propagated with copying and pasting. When discovered, they must be fixed in several different locations. New mistakes can also be introduced by cutting and pasting. Instead, we wish to guarantee that working code from a parent class continues to work in a child class. Ideally, code from parent classes will not need to be debugged a second time when a child class is created.

Even without the issue of errors introduced by copying and pasting, the total amount of code increases. By minimizing the amount of code, issues of performance and storage can be improved, but not always. Object-oriented languages have taken criticism for low speed and high memory use due to the additional complexities of objects and inheritance, but compiler optimization, good library design, and improved JVM performance have brought Java a long way in this area.

11.3. Syntax: Inheritance in Java

In this section we discuss the mechanism for creating a child class in Java using the extends keyword. Then, we discuss access restriction and visibility, constructor issues, the Object class, and overriding methods.

11.3.1. The extends keyword

In order to make a child class in Java, we use the extends keyword. Let’s give an example using the Fish class defined below. This class creates a basic fish that can swim, feed, and die. We can check its color, location, and whether or not it’s alive. When it runs out of energy, it dies.

import java.awt.*;

public class Fish {
    protected Color color = Color.GRAY;
    private double location = 0.0;
    private double energy = 100.0;
    private boolean alive = true;
    
    public Color getColor() { return color; }
    public double getLocation() { return location; }
    public boolean isAlive() { return alive; }
    
    public void swim() {
        if(alive) {
            location += 0.5;
            energy -= 0.25;
        }
        if(energy <= 0.0)
            die();
    }
    
    public void feed() { energy = 100.0; }
    public void die() { alive = false; }
}

From here we can create a child class called BoringFish that does exactly what Fish does. To do so, we use the extends keyword after the new class name, followed by the parent class name (in this case Fish), followed by the body of the class.

public class BoringFish extends Fish {
}

Just as we’re allowed to make an empty class, we’re allowed to make an inherited class and add nothing, but doing so is pointless. Instead, we can make a Flounder class that can change its color.

import java.awt.*;

public class Flounder extends Fish {
    public void setColor(Color newColor) { color = newColor; }
}

The Flounder class can do everything a Fish can: It can swim, feed, and die. But we also add the ability to change color since flounders are famous for their ability to mimic the ocean floor they swim over. Note that the color field in the Fish class has the protected access modifier, not private. We’ll come back to this point.

Here’s a Shark class that extends Fish in another way, by adding the capability of eating other Fish.

public class Shark extends Fish {
    public void eat(Fish fish) {
        fish.die();
        feed();     
    }
}

Here we have added an eat() method that takes another Fish object as a parameter. First, the Fish parameter is killed; then the eat() method calls feed(), restoring the energy of the Shark object. Note that the Shark object is able to call the feed() method even though it isn’t defined inside of Shark. Because it inherits from Fish, it has a version of feed().

Single inheritance only

Particularly if you’ve programmed in C++, you might be wondering if it’s possible to have one class inherit from multiple classes in Java. In multiple inheritance, a single class can have many different parents. Since C++ supports multiple inheritance, it would allow you to have a SharkAlligatorMan class that inherits from the Shark, Alligator, and Human classes. If you go back to the sorting problem from Chapter 10, multiple inheritance would allow us to solve the problem with an Age class and a Weight class from which Dog, Cat, Person, and Cheese all inherit.

However, the designers of Java decided not to allow multiple inheritance, perhaps for this reason: Imagine a River class with a run() method and a Politician class with a run() method. It seems strange to create a class which is both a river and politician, but there is no rule in C++ which makes doing so impossible. If you did have a RiverPolitician class which inherits from both, what would happen when you call the run() method? How would the RiverPolitician class know which of its parents' methods to pick? Surely, the way that a politician runs for office is very different from the way a river runs along its banks.

This problem is similar to the issue discussed in Section 10.3.1, where a class could implement more than one interface with the same default method; however, the problem is more severe in the case of multiple inheritance since it becomes unclear which fields inside of parent classes are being referred to, not just which methods.

If you find yourself in a situation where you want to use multiple inheritance in Java, try to reformulate your class hierarchy into one where your classes implement multiple interfaces. Recall that multiples interfaces can be implemented by a single class in Java, and like multiple inheritance, this practice allows a single class to be used in wildly different contexts.

Interfaces using extends

The extends keyword is not limited to classes. It’s possible for an interface to extend another interface. In fact, an interface can extend any number of other interfaces. As when a class implements multiple interfaces, each interface in an extends list is separated by commas.

When an interface extends other interfaces, it includes all the methods (and constants) they define. If a class implements an interface that extends other interfaces, it must contain versions of all the methods specified by all the interfaces. Recall the Ageable and Weighable interfaces from Chapter 10, which specified the getAge() and getWeight() methods, respectively. We could create an interface that required both of these methods by extending Ageable and Weighable.

public interface AgeableAndWeighable extends Ageable, Weighable {
}

We could add additional methods to the AgeableAndWeighable interface, but even empty it will enforce the contracts defined by both Ageable and Weighable. It’s usually not necessary to create an interface that extends other interfaces, since a class could implement each of the individual interfaces. Nevertheless, it can be used as a convenience to save typing or to create a reference type with certain guaranteed abilities.

Note that a class can never extend an interface. Likewise, an interface cannot extend a class or implement another interface.

11.3.2. Access restriction and visibility

The Shark example above gives an example of inheritance in which the child class only calls methods of the parent class and does not interfere with the fields of the parent class. Generally, leaving parent fields alone is a good thing because it protects the state of the parent class from getting corrupted. However, it’s not always possible. If we return to the earlier Flounder example, we had to change the color field directly since there was no mutator to change it.

Perhaps the Fish class was poorly designed because it didn’t have a color mutator. On the other hand, most fish cannot change their color, so it might be good design to prevent outside code from changing the color field with such a mutator. There are no absolute rules for making these kinds of decisions.

We introduced access modifiers in Section 9.3.4, but inheritance gives them new meaning. Recall that the access modifier for the color field of Fish was protected. A field or method with the protected modifier can be accessed by all child classes (as well as classes in the same package). If the modifier for color was private, the Flounder class would not be able to change it directly.

In the Shark class, it must use mutators to change the value of its own energy and the alive field of the fish object it eats since they’re both marked private. It’s generally preferable to use mutator and accessor methods whenever possible, even within the same class, so that fields are not inadvertently corrupted.

11.3.3. Constructors

When you create a child class, you can imagine that a copy of the parent class exists inside of the child. When you create an object from a child class, how do you properly initialize the fields inside the parent class?

As we discussed in Chapter 9, every class has a constructor, even if it’s a default one created for you. Whenever the constructor for a child class is invoked, the constructor for the parent class is invoked as well. If the parent class is also the child of some other class, that grandparent class will have its constructor invoked as well. This chain of constructors will continue, reaching all the way back to the ultimate ancestor, Object.

When writing the constructor for a child class, the first line of it should be the call to the parent constructor. If you don’t explicitly call the parent constructor, its default (no parameter) constructor will be called. If the parent class does not have a default constructor, then leaving off an appropriate call to a parent constructor will result in a compiler error. Consider the following two classes.

public class Parent {
    private String name;
	
    public Parent(String name) { this.name = name; }
    public String getName() { return name; }
}
public class Child extends Parent {
    public Child(String name) {
        super("Baby " + name);
    }
}

As shown above, the super keyword is used to call the constructor of a parent class. The Child constructor takes a name and prepends the String "Baby " to it before passing it on to the Parent constructor.

In a similar way, the this keyword can be used to call another constructor in the same class, provided that a constructor to the parent class is eventually reached. For example, we could add the following constructor to the Child class.

    public Child() {
        this("Unknown");
    }

This second constructor will be called whenever a new Child object is instantiated without any arguments. It will supply the String "Unknown" to the other constructor, which will add "Baby" and pass it on to the Parent class.

11.3.4. Overriding methods and hiding fields

Sometimes a parent method doesn’t provide all the power you want in the child class. It’s possible to override a parent method in the child class. Then, when that method is called on child objects, the new method will be called. The new method has exactly the same name and parameters. The return type must either be exactly the same or a child class of the original return type.

We can return to the Fish class example and make a new kind of fish that never moves.

public class LazyFish extends Fish {
    public void swim() {
        System.out.println("I think I'll just sit here.");
    }
}

Whenever someone calls the swim() method on a LazyFish object, it will announce that it’s going to sit where it is. Its location isn’t updated, and its energy doesn’t change.

On the other hand, we could create another child class that swims twice as fast as the original Fish.

public class FastFish extends Fish {
    public void swim() {
        super.swim();
        super.swim();
    }
}

Every time swim() is called on objects of type FastFish, those objects will call the swim() method from Fish twice. Thus, this fish will move twice as fast (and consume twice as much energy). Because the location and energy fields are private, we must use methods from Fish to affect them. Note the use of the keyword super, allowing us to specify that we want to call the swim() method from Fish and not just call the same method from FastFish again. Using the super keyword, we can call methods from the parent. If the parent didn’t override a method from an ancestor class, we can still use super to call a method from the most recent ancestor class that did implement the method. However, Java does not allow us to skip over a parent method to call a grandparent method if there’s an implementation in the parent class. In other words, there’s no way to call something like a super.super.swim() method.

Just as methods are overridden, fields are hidden. It’s perfectly legal to declare a field with the same name as a field from a parent class, but the new field will then be used instead of the old one.

public class A {
    protected int a;
	
    public int getA() { return a; }
    public void setA(int value) { a = value; }
}
public class B extends A {
    protected int a;
	
    public void setA(int value) { a = value; }
}

Class B is a child of class A and declares a field called a, hiding a field of the same name from A. However, which a is which can cause some confusion. Consider the following fragment of code.

A objectA = new A();
B objectB = new B();
objectA.setA(5);
objectB.setA(10);
System.out.println("A = " + objectA.getA());
System.out.println("B = " + objectB.getA());

The output of this code is:

A = 5
B = 0

Calling the setA() method on an A object sets the a field inside of A. Calling the overridden setA() method on a B object sets the a field inside of B, but since the getA() method hasn’t been overridden, the a field from the A parent class part of B is returned. Since that a field in B hasn’t been given a value, it still has the default value of 0. Both a fields exist inside of B, but the methods are poorly designed, leaving one field capable only of being set and the other capable only of being retrieved.

11.3.5. The Object class

You may not have realized it, but every class you’ve created in Java uses inheritance. To provide uniformity, the designers of Java made every class the child (or grandchild or great-grandchild…​) of a class called Object. When you omit the extends clause in a class definition, you’re making that class a direct child of Object.

As a consequence, all classes in Java are guaranteed to have the following methods.

Method Purpose

clone()

Make a separate copy of an object.

equals()

Determine if two objects are the same.

finalize()

Perform cleanup when an object is garbage collected. Similar to a destructor in C++. Rarely used.

getClass()

Find out what the class type of a given object is.

hashCode()

Get the hash code for an object, useful for making hash tables of objects.

notify()

Used for synchronization with threaded programs. More in Chapter 14.

notifyAll()

Same as previous.

toString()

Get a String representation of the given object.

wait()

Used with notify() and notifyAll().

Java provides basic implementations for most of these, but if you want them to work well for your object, you’ll have to override some of them with appropriate methods. For example, the Object version of toString() returns the virtual address of the object in JVM memory, which is not very useful information.

Nevertheless, API classes usually have good equals() and toString() methods. Aside from making a few useful methods available, having a common ancestor for all classes means that you can store any object in an Object reference. An array of type Object can hold anything, provided that you know how to retrieve it. We discuss the finer points of inheritance and polymorphism in Chapter 17 and how to build lists and other data structures using Object references in Chapter 18.

11.4. Examples: Problem solving with inheritance

Here are two extended examples showing how we can use inheritance to solve problems. First, we revisit the student roster example from Chapter 9 and then move onto an inheritance hierarchy of polygons.

Example 11.1 Graduate student roster

The Student class we created in Example 9.1 is useful but works only for undergraduate students. With only a few additions, we can make it suitable for graduate students as well. First, let’s take another look at the Student class.

Program 11.1 Basic student class, designed for undergraduates.
public class Student {
    public static final String[] YEARS = {"Freshman", "Sophomore", "Junior", "Senior"};
    private String name;
    private int year;
    private double GPA;

    public Student(String name, int year, double GPA) {
        setName(name);
        setYear(year);
        setGPA(GPA);
    }   

    public void setName(String name) { this.name = name; }  
    public void setYear(int year) { this.year = year; }

    public void setGPA(double GPA) {
        if(GPA >= 0 && GPA <= 4.0)
            this.GPA = GPA;
        else
            System.out.println("Invalid GPA: " + GPA);      
    }

    public String getName() { return name; };
    public int getYear() { return year; };
    public double getGPA() { return GPA; };

    public String toString() {
        return name + "\t" + YEARS[year] + "\t" + GPA;
    }   
}

We want to create a GraduateStudent class that inherits from Student. We need to add a thesis topic for each graduate student. Likewise, we need to update the toString() method so that outputs the appropriate data. We use 4 as the year value for graduate students.

Program 11.2 Class extending Student to add graduate student capabilities.
public class GraduateStudent extends Student {
    private String topic; (1)

    public GraduateStudent(String name, double GPA, String topic) {
        super(name, 4, GPA); (2)
        setTopic(topic);
    }   

    public void setTopic(String topic) { this.topic = topic; }  

    public String toString() { (3)
        return getName() + "\tGraduate\t" + getGPA() + "\tTopic: " + topic;
    }   
}
1 Because we’re inheriting most of the fields we need, we only need to declare the topic field.
2 Then, in the GraduateStudent constructor, we call the parent constructor with the name, year, and GPA and then set topic to the input value.
3 Finally, we override the toString() method so that "Graduate" and the thesis topic are output. Note that we must use the getName() and getGPA() accessors since those fields are private in Student.

Most code that uses Student objects should be able to incorporate GraduateStudent objects easily. Code that creates Student objects from input will need slight modifications to handle the thesis topic. Also, old code that only expects values of 0, 1, 2, or 3 for year may need to be modified so that it doesn’t break.

Example 11.2 Polygons

Let’s examine a class hierarchy used to create several different polygons. Our base class needs to be general. It can represent any kind of closed polygon, using an array of Point objects. The Point library class is a way to package up x and y values of type int. Each coordinate in the array gives the next vertex of the polygon.

import java.awt.*; (1)

public class Polygon {
    protected Point[] points; (2)

    public Polygon(Point[] points) { (3)
        this.points = points;
    }
1 The import statement allows us to use the Point class as well as the Graphics class.
2 Our array of type Point is declared protected so that the child classes we want to create can access it directly.
3 The constructor takes an array of type Point and stores it.
    public double getPerimeter() { (1)
        double perimeter = 0.0;
        for(int i = 0; i < points.length - 1; i++)
            perimeter += points[i].distance(points[i + 1]);
        perimeter += points[0].distance(points[points.length - 1]);
        return perimeter;
    }

    public void draw(Graphics g) { (2)
        for(int i = 0; i < points.length - 1; i++)
            g.drawLine(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
        g.drawLine(points[0].x, points[0].y,
			points[points.length - 1].x, points[points.length - 1].y);
    }
}
1 The getPerimeter() method can determine the length of the perimeter by adding the lengths of the segments connecting the vertices. Although it’s also possible to determine the area enclosed by a list of vertices, the algorithm is complex.
2 The draw() method draws the polygon by drawing each line segment that connects adjacent vertices. We discuss the Graphics class in Chapter 15. If you compile and run this code, please note that in Java graphics, like many computer graphics environments, the upper left hand corner of the screen or window is considered (0,0), and y values increase going downward, not upward.

The number of things that can be done with this very general Polygon class are limited, but with this basic parent class defined, we can design a Triangle class as a child of it.

import java.awt.*; (1)

public class Triangle extends Polygon {
    public Triangle(int x1, int y1, int x2, int y2, int x3, int y3) { (2)
        super(toPointArray(x1, y1, x2, y2, x3, y3));
    }
    
    protected static Point[] toPointArray(int x1, int y1, int x2, int y2, int x3, int y3) {  
        Point[] array = {new Point(x1, y1), new Point(x2, y2), new Point(x3, y3)}; (3)
        return array;
    }
1 Again, the import statement is for the Point class.
2 One reasonable constructor for a triangle takes in six values, giving the x and y coordinates of the three vertices of the triangle. Of course, the Polygon class requires an array of type Point, but the super constructor must be the first line of the Triangle constructor.
3 To solve this problem, we create a static method to package the values into an array. We could have done the same thing in the argument list of the super constructor, but it would have looked messier. The toPointArray() is protected because there’s no reason to let external code have access to it.
    public String getType() {
        double a = points[0].distance(points[1]);
        double b = points[1].distance(points[2]);
        double c = points[2].distance(points[0]);
        if(a == b && b == c)
            return "Equilateral";
        if(a == b || b == c || a == c)
            return "Isosceles";
        return "Scalene";
    }
}

Finally, the getType() method allows us to do something specific with triangles. We can use the distance() method from the Point class to find the length of each of the three sides. By comparing these lengths, we can determine whether the triangle represented is equilateral, isosceles, or scalene. Of course, computing the perimeter and drawing the triangle are already taken care of by the Polygon class.

We can easily make a Rectangle class along the same lines.

import java.awt.*;

public class Rectangle extends Polygon {
    public Rectangle(int x, int y, int length, int width) { (1)
        super(toArray(x, y, length, width));
    }

    protected static Point[] toArray(int x, int y, int length, int width) { (2)
        Point[] array = {new Point(x, y), new Point(x + length, y),
            new Point(x + length, y + width), new Point(x, y + width)};
        return array;
    }

    public int getArea() { (3)
        int length = points[1].x - points[0].x;
        int width = points[2].y - points[1].y;
        return length * width;
    }
}
1 The constructor is similar to the Triangle constructor except that the upper left corner of the rectangle is specified, along with the length and the width.
2 From these values, the appropriate array of Point values is generated.
3 The rectangle-specific code that we add is the getArea() method, which determines the length and width of the rectangle by examining the points array and then calculates area.

Using inheritance as form of specialization, we can go one step further and make a Square class.

public class Square extends Rectangle {
    public Square(int x, int y, int size) {
        super(x, y, size, size);
    }
}

This very short class uses everything available in Rectangle but simplifies the constructor slightly so that the user doesn’t have to enter both length and width.

11.5. Solution: Boolean circuits

Here we present our solution to the Boolean Circuits problem. First, we define a parent class for all circuit components, called Gate.

public class Gate {
    private String name;

    public Gate(String name) { this.name = name; }
    public String getName() { return name; }
    public String toString() {
        return getName() + ": " + getValue();
    }
    public boolean getValue() { return false; }
}

The Gate class doesn’t do anything except set up ways to store a name and to get a value. It doesn’t really matter what getValue() gives back for Gate, but we can say that it’s false.

In principle, it shouldn’t be possible to create an object of type Gate, UnaryOperator, or BinaryOperator. Classes that are designed only to be parent classes and never to be instantiated are called abstract classes and are discussed in Section 17.3.1.

From Gate, we can define the most basic circuit components: gates whose value is either always true or always false.

public class True extends Gate {
    public True() { super("true"); }
    public boolean getValue() { return true; }
}
public class False extends Gate {
    public False() { super("false"); }
    public boolean getValue() { return false; }
}

To conform with the constructor for Gate, these new classes must pass a String giving their name to the super constructor. The values returned by the getValue() method are clear. Next, we want to create a class that can be used as a parent for all unary operators.

public class UnaryOperator extends Gate {
    private Gate input;

    public UnaryOperator(String name) { super(name); }
    public void setInput(Gate input) { this.input = input; }
    public Gate getInput() { return input; }
}

The important addition in the UnaryOperator class is the input field. Any unary operator must have a single input gate that it operates on. This class provides a mutator and accessor for input, as well as an appropriate constructor. From UnaryOperator, we can derive two specific operators.

public class Output extends UnaryOperator {
    public Output(int i) { super("OUTPUT " + i); }
    public boolean getValue() { return getInput().getValue(); }
}

The Output class takes in an int value and uses it to make a numbered name. Its getValue() method simply returns the value of its input. The Output class doesn’t do anything except serve as a marker for circuit output.

public class Not extends UnaryOperator {
    public Not() { super("NOT"); }
    public boolean getValue() { return !getInput().getValue(); }
}

The Not class uses "NOT" as the name supplied to the super constructor and returns the logical NOT of the value of its input.

Just as we did for unary operators, we also need a parent class for binary operators.

public class BinaryOperator extends Gate {
    private Gate operand1;
    private Gate operand2;

    public BinaryOperator(String name) { super(name); }
    public Gate getOperand1() { return operand1; }
    public Gate getOperand2() { return operand2; }
    public void setOperand1(Gate operand) { operand1 = operand; }
    public void setOperand2(Gate operand) { operand2 = operand; }
}

A BinaryOperator has two Gate fields, operand1 and operand2, representing the inputs to the operator. The BinaryOperator class has an appropriate constructor and then accessors and mutators for the operands. With BinaryOperator as a parent, only a few lines of code are necessary to define any logical binary operator.

public class And extends BinaryOperator {
    public And() { super("AND"); }

    public boolean getValue() {
        return getOperand1().getValue() && getOperand1().getValue();
    }
}
public class Or extends BinaryOperator {
    public Or() { super("OR"); }

    public boolean getValue() {
        return getOperand1().getValue() || getOperand1().getValue();
    }
}
public class Xor extends BinaryOperator {
    public Xor() { super("XOR"); }

    public boolean getValue() {
        return getOperand1().getValue() ^ getOperand1().getValue();
    }
}

In each case, a constructor passes the name of the gate to the super constructor. Then, each getValue() method gets the values from the two operands and combines them with AND, OR, or XOR, respectively. This design allows the programmer to focus only on the important element of each class. Adding new classes for NAND, NOR, or any other possible logical binary operator would be quick.

The client code that uses these classes to simulate a circuit follows.

import java.util.*;

public class BooleanCircuit {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int count = in.nextInt();
        Gate[] gates = new Gate[count];
        String name;
        int value;

First we have the import needed for Scanner. In main(), we read in the total number of gates, create an array of type Gate of that length, and declare a few useful temporary variables.

        // Create gates
        for(int i = 0; i < count; i++) {
            name = in.next().toUpperCase();
            if(name.equals("true"))
                gates[i] = new True();
            else if(name.equals("false"))
                gates[i] = new False();
            else if(name.equals("AND"))
                gates[i] = new And();
            else if(name.equals("OR"))
                gates[i] = new Or();
            else if(name.equals("XOR"))
                gates[i] = new Xor();
            else if(name.equals("NOT"))
                gates[i] = new Not();
            else if(name.equals("OUTPUT")) {
                value = in.nextInt();
                gates[i] = new Output(value);
            }
        }       

Then, we parse the input, creating an appropriate gate based on the name read in. In the case of an OUTPUT gate, we must also read in a number so that we can identify which OUTPUT gate is which later.

        //connect gates
        while(in.hasNextInt()) {
            value = in.nextInt();
            name = gates[value].getName();
            if(name.equals("AND") || name.equals("OR") || name.equals("XOR")) {
                BinaryOperator operator = (BinaryOperator)gates[value];
                operator.setOperand1(gates[in.nextInt()]);
                operator.setOperand2(gates[in.nextInt()]);
            }
            else if(name.equals("NOT") || name.startsWith("OUTPUT")) {
                UnaryOperator operator = (UnaryOperator)gates[value];
                operator.setInput(gates[in.nextInt()]);
            }
        }

As long as there’s input remaining, we read in an index. Based on the name of the gate at that index in the array, we either read in two more indexes (for binary operators) or just a single additional index (for unary operators). In either case, we set the input or inputs of the operator to the gate or gates at those indexes.

        // Compute output
        for(int i = 0; i < count; i++)
          if(gates[i].getName().startsWith("OUTPUT"))
            System.out.println(gates[i]);
      }
}

Finally, the simulation of the circuit is surprisingly simple. We look through array until we find a gate whose name starts with "OUTPUT". Then, we print out its value. In order to determine its value, it will ask its input what its value is, which in turn will ask for the values from its input. The toString() in the Gate class will assure us that the final output is nicely formatted. This system accommodates any number of output gates connected arbitrarily, as long as the circuit has no loops inside of it, such as an AND gate whose output is also one of its inputs.

11.6. Concurrency: Inheritance

Like interfaces, inheritance in Java is not closely related to concurrency. However, two ways in which inheritance interacts with concurrency deserve attention.

The first is the Thread class. Each thread of execution in Java (except the main thread) is managed with a Thread object or an object whose type inherits from Thread. Creating such types is done by extending Thread, just as you would extend any other class. Further information about extending Thread for concurrency is given in Section 13.4. Extending the Thread class to make your own customized threads of execution is an alternative to implementing the Runnable interface mentioned in Section 10.6 and is discussed in greater detail in Section 13.4.5.

The second interaction between inheritance and concurrency is again very similar to the problem with interfaces and concurrency: There’s no way to specify that a method is thread-safe. Recall that it’s not allowed to use the synchronized keyword on a method in an interface declaration. Likewise, there’s no restriction on overriding a synchronized method with a non-synchronized method or vice versa.

The rules for overriding methods in Java guarantee that an object of a child class is usable anywhere that an object of the parent class is usable. Thus, you cannot override a public method with a private one, reducing the visibility of a method. We discuss a similar restriction with exceptions in Section 17.3.4.

If it has these restrictions, why doesn’t Java prevent a synchronized method from being overridden by a non-synchronized method? In the first place, a non-synchronized method can be used anywhere a synchronized one could (unlike a private method, which is not accessible everywhere a public one is). In the second, the designers of Java put thread safety in the category of implementation details left up to the programmer. Some classes need specific methods to be synchronized and others (even child classes) do not. However, if you override a class with a synchronized method, it’s safest to mark your method synchronized as well.

11.7. Exercises

Conceptual Problems

  1. Give three advantages of using inheritance instead of copying and pasting code from a parent class. Are there any disadvantages to using inheritance?

  2. Consider classes Radish and Carrot which both extend class Vegetable and implement interface Crunchable. Which of the following sets of assignments are legal and why?

    1. Radish radish = new Radish();

    2. Radish radish = new Vegetable();

    3. Vegetable vegetable = new Radish();

    4. Crunchable crunchy = new Radish();

    5. Radish radish = new Carrot();

  3. In the context of inheritance, the keyword super can be used for two different purposes. What are they?

  4. Consider the following class definitions.

    public class A {
        private String value;
        public A(String s) { value = "A" + s + "A"; }
        public String toString() { return value; }
    }
    
    public class B extends A {
        public B(String s) { super("B" + s + "B"); }
    }
    
    public class C extends B {
        public C(String s) { super("C" + s + "C"); }
    }

    What’s output by the following code fragment?

    C c = new C("ABC");
    System.out.println(c);
  5. Beginning Java programmers often confuse package-private access (no explicit specifier) with public access. How is this confusion possible when default access is more constrained than both public and protected access? (Hint: The file system plays a role.)

  6. What are the similarities and differences between overloading a method and overriding a method?

  7. What’s field hiding? How can software bugs arise from this Java feature?

  8. Give reasons why the designers of Java decided not to allow multiple inheritance. Would you have made the same decision? Why or why not?

  9. Draw a class hierarchy establishing a sensible relationship between the Human, Soldier, Sailor, Marine, General, and Admiral classes. For this class hierarchy, refer to the U.S. military structure in which the U.S. Marine Corps is a part of the U.S. Navy.

Programming Practice

  1. Create an InternationalStudent class that extends Student. It should include String fields for country of origin and visa status. It should include mutator and accessor methods for these two new fields.

  2. Add Pentagon and Hexagon classes that extend the Polygon class. The constructor for each class should take an x, y and radius value, each of int type. Both classes should be implemented to create regular polygons, that is, polygons in which all five or six sides have the same length. The x and y values should give the center of the polygon, and each of the five or six points should be the radius distance away from that center.

    Because the internal structure of Polygon keeps all vertices as Point values, the x and y coordinates of the points must be int values. This requirement will force you to round these x and y coordinates after using trigonometry to determine their locations. As a result, the final pentagons and hexagons stored and displayed will be slightly irregular.

  3. The inheritance design of our solution to the Boolean circuits problem given in Section 11.5 makes adding new gates easy. Add classes that implement a NAND gate and a NOR gate. Then, rewrite the main() method of BooleanCircuit to accommodate these two extra classes.

  4. Re-implement the object hierarchy in the solution from Section 10.5 to the sort it out problem. This time, let the Cat, Dog, and Person classes extend the Creature class defined below.

    public class Creature implements Ageable, Weighable {
        protected int age;
        protected double weight;
        
        public Creature(int age, double weight) {
            this.age = age;
            this.weight = weight;
        }
        
        public int getAge() { return age; }    
        public double getWeight() { return weight; }
    }

    Refactor your code so that the Cat, Dog, and Person classes are as short as possible. How many lines of code do you save?

  5. Design a celestial body simulator. You’ll need to create a class containing fields for the x, y, and z locations, x, y, and z velocities, radii, and masses of each object. For each time step of length t, you must do the following.

    1. Compute the sum of forces exerted on each body by every other body. The equation for gravitational force on body b exerted by body a is given by the following equation.

      force

      As mentioned in Example 8.1, the gravitational constant G = 6.673 × 10-11 N⋅m2⋅kg-2. Also, |rab| is the distance between the centers of objects a and b. Finally, the unit vector between the centers of the two objects is given by the equation below.

      unitvector
    2. Compute the x, y, and z components of the acceleration vector a for each object using the equation F = ma once the sum of forces has been calculated.

    3. Update the x, y, and z components of the velocity vector v for each object using the equation vnew = vold + at.

Experiments

  1. Inheritance is a powerful technique, but it comes with some overhead costs. Create a class called A with the following implementation.

    public class A {
        protected int a;
    }

    Then, create 25 more classes named B through Z. Class B should extend A and add a protected int field called b. Continue in this manner, with each new class extending the previous one and adding an int field named the lowercase version of the class name. Thus, if you create an object of type Z, it will contain, through inheritance, 26 int fields named a through z. But for a single Z object to be created, it must call 27 (Z back through A plus Object) constructors. You may wish to use the file I/O material in Chapter 20 to write a program to create all these classes so that you do not have to do so by hand.

    Finally, create a new class called All which contains 26 protected fields of int type named a through z. Now, the purpose of creating all these classes is to compare the time needed to instantiate an object of type Z with one of type All, though they both only contain 26 int fields named a through z.

    Create an array of 100,000 elements of type Z and then populate it with 100,000 Z objects. Time this process. Create an array of 100,000 elements of type All and then populate that array with 100,000 All objects. You may wish to use the System.nanoTime() method described in Chapter 13 to accurately time these processes. Is there a significant difference in the times you found?

12. Exceptions

The vulgar mind always mistakes the exceptional for the important.

— W. R. Inge

12.1. Problem: Bank burglary

Let’s consider a problem in which various aspects of a bank burglary are modeled as Java objects. You want to write some code that will accomplish the following steps:

  1. Disable the burglar alarm

  2. Break into the bank

  3. Find the vault

  4. Open the vault

  5. Carry away the loot

But any number of things could go wrong! When trying to disable the burglar alarm, you might set it off. When you try to break into the bank, you might have thought that you’d disabled the burglar alarm but actually failed to do so. The door of the bank might be too difficult to open. The vault might be impossible to find, or the vault might be impossible to open. It could even be empty! The money might be made of enormous gold blocks that are too heavy to carry away. Finally, at any time during the heist, a night watchman might catch you in the act.

If you’re the criminal mastermind who planned this deed, you need to know if (and preferably how) your henchman bungled the burglary. You need a simple system that can inform you of any errors that have occurred along the way. Likewise, you need to be able to react differently depending on what went wrong.

We are going to model each of these error conditions with Java exceptions. An exception is how Java indicates exceptional or incorrect situations inside of a program. These exceptions are thrown when the error happens. You must then write code to catch the error and deal with it appropriately. The bank burglar program will deal with three different classes, Bank, Vault, and Loot.

The Bank class has three methods, disableAlarm(), breakIn(), and findVault(), which returns a Vault object. The Vault class has an open() method and a getLoot() method, which returns a Loot object. The Loot class has a carryAway() method. Below is a table of the exceptions which can be thrown by each of the methods.

Class Method Exceptions
Bank
disableAlarm()
BurglarAlarmException
WatchmanException
breakIn()
BurglarAlarmException
LockPickFailException
WatchmanException
findVault()
WatchmanException
Vault
open()
LockPickFailException
WatchmanException
getLoot()
WatchmanException
Loot
carryAway()
LootTooHeavyException
WatchmanException

In order to deal with each of these possible errors, you need some special Java syntax.

12.2. Concepts: Error handling

As a rule, computer programs are filled with errors. Writing a program is a difficult and complex process. Even if a segment of code is free from errors, it may call other code which contains mistakes. The user could be making mistakes and issuing commands to a program that are impossible to execute. Even hardware can produce errors, as in a hard drive crash or a network connectivity problem.

12.2.1. Error codes

A robust program should deal with as many errors as possible. One strategy is to have every method give back a special error code corresponding to an error when it occurs. Then, the code calling the method can react appropriately. Of course, many methods do not return a numerical type, limiting this kind of error handling. A solution that was very common in the C language, particularly in Unix system calls, was to set a globally visible int variable called errno to a value corresponding to the error that has just happened.

These approaches have a number of drawbacks. In the case of errno, if a number of different threads were running at the same time, different errors could occur simultaneously, but only one value could be kept in errno. For any system that relies on checking for an error condition after each method call, a large amount of error handling code must be mixed in with normal code. Doing so reduces code readability and makes it difficult to handle errors in a central place. Likewise, a numerical value doesn’t describe the error, requiring good documentation to know what the number means.

12.2.2. Exceptions

Java adopts a different error handling strategy called exceptions. Whenever a specified error state or unusual situation is reached, an exception is thrown. When an exception is thrown, normal execution stops immediately. The JVM starts backtracking, looking for code that’s designed to deal with that specific exception. The code that will handle the exception can be in the current method, in the calling method, in the caller of the calling method, or arbitrarily far back in the chain of method calls, all the way to main(). Each method will return, looking for code to handle this exception, until it’s found. If no handling code is found, the exception will propagate all the way past main(), and the program will end.

Exceptions give a unified and simple way to handle all errors. You can choose to deal with errors directly or delegate that responsibility to methods that call the code you’ve written. Selection statements and loops are forms of local control flow, but exceptions give us the power of non-local control flow, able to jump back through any number of method calls.

12.3. Syntax: Exceptions in Java

In Java syntax, there are two important sides of using exceptions: throwing the exception when an error occurs and then handling that exception properly. Below we explain both of these as well as the catch or specify requirement, the finally keyword, and the process of creating custom exceptions.

12.3.1. Throwing exceptions

By now you’ve probably experienced a NullPointerException in the process of coding. This exception happens when an object reference is null but we try to access one of its methods or fields.

String text = null;
int x = text.length(); // NullPointerException

In this case, the exception is thrown by the JVM itself. It is possible to catch this exception and deal with it, but a NullPointerException generally means a mistake in the program, not an error that can be recovered from. Although many useful exceptions such as NullPointerException, ArithmeticException, and ArrayIndexOutOfBoundsException are implicitly thrown by the JVM, we’re also allowed to throw them explicitly.

if(y < 14)
    throw new NullPointerException();

Like any other object, we use the new keyword to instantiate a NullPointerException using its default constructor. Once created, we use the throw keyword to cause the exception to go into effect. Any exception you throw explicitly must use the throw keyword, but the majority of exceptions thrown by your programs will either be mistakes or exceptions thrown by library code you’re calling. If you write a significant amount of library or API code, you might use throw more often.

12.3.2. Handling exceptions

Normal application programmers will find themselves writing code that handles exceptions much more often than code that throws them. In order to catch an exception, you must enclose the code you think is going to throw an exception in a try block. Immediately after the try block, you can list one or more catch blocks. The first catch block that matches your exception will be executed.

try {
    String text = null;
    int x = text.length(); // NullPointerException
    System.out.println("This will never be printed.");
}
catch(NullPointerException e) {
    System.out.println("Surprise! A NullPointerException!");
}

In this case, trying to access the length() method of a null reference will still throw a NullPointerException, but now it’ll be caught by the catch block below. The message "Surprise! A NullPointerException!" will be printed to the screen, and execution will continue normally after the catch block. Once the exception is caught, it stops trying to propagate. Of course, whatever the code was doing when the exception was thrown was abandoned immediately because it might have depended on successful execution of the code that threw the exception. Thus, the call to the System.out.println() method in the try block will never be executed.

An exception will match the first catch block with the same class or any superclass. Since Exception is the parent of RuntimeException which is the parent of NullPointerException, we could write our example with Exception instead.

try {
    String text = null;
    int x = text.length(); // NullPointerException
    System.out.println("This will never be printed.");
}
catch(Exception e) {
    System.out.println("Well, of course you got a NullPointerException!");
}

In general, you should write the most specific exception class possible for your catch blocks. Otherwise, you might be catching a different exception than you planned for, preventing that exception from propagating up to an appropriate handler. For example, the following code will randomly throw either a NullPointerException or an ArithmeticException (because of a division by 0).

try {
    String text = null;
    int x;
    if(Math.random() > 0.5)
        x = text.length(); // NullPointerException
    else
        x = 5 / 0; // ArithmeticException
}
catch(Exception e) {
    System.out.println("You got some kind of exception!");
}

This code will catch either kind of exception, but it won’t tell you which you got. Instead, the correct approach is to have one catch block for each possible kind of exception.

try {
    String text = null;
    int x;
    if(Math.random() > 0.5)
        x = text.length(); // NullPointerException
    else
        x = 5 / 0; // ArithmeticException
}
catch(NullPointerException e) {
    System.out.println("You used a null pointer!");
}
catch(ArithmeticException e) {
    System.out.println("You divided by zero!");
}

The list of catch blocks can be arbitrarily long. You must always go from the most specific exceptions to the most general, like Exception, otherwise some exceptions could never be reached. The Java compiler enforces this requirement. The e is a reference to the exception itself, which behaves something like a parameter in a method. It’s common to use e as the identifier, but you’re allowed to call it any legal variable name. Usually, the kind of exception is all you need to know, but every exception is an object and has fields and methods. Particularly useful is the getMessage() method which can give additional information about the exception.

12.3.3. Catch or specify

In contrast to the examples given above, you’ll rarely write code to catch a NullPointerException or an ArithmeticException. Both of these exceptions are called unchecked exceptions. In Chapter 6, we used the Thread.sleep() method to put the execution of our program to sleep for a short period of time. We were forced to enclose this method call in a try block with a catch block for InterruptedException.

try{
    Thread.sleep(100);
}
catch(InterruptedException  e) {
    System.out.println("Wake up!");
}

An InterruptedException is thrown when another thread tells your thread of execution to wake up before it finishes sleeping or waiting. This exception is a checked exception, meaning that Java insists that you use a try-catch pair anytime there’s even a chance of it being thrown. Otherwise, your code won’t compile.

Checked exceptions are those exceptions that your program must plan for. Library and API code often throw checked exceptions. For example, when trying to open a file with an API call, it’s possible that no file with that name exists or that the user might not have permission to access it. A program should catch the corresponding exceptions and recover rather than crashing. Perhaps the program should prompt the user for a new name or explain that the required permission is not set.

In Chapter 6, there were no executable statements in the catch block used with the Thread.sleep() method. However, you should never write an empty catch block. Doing so allows errors to fail silently.

We’re allowed to put code that can throw a checked exception into a try-catch block, but there’s another option. Java has a catch or specify requirement, meaning that your code is required either to catch a checked exception or to specify that it has the potential for causing that exception. To specify that a method can throw certain exceptions, we use the throws keyword. Note that this is not the same as the throw keyword.

public static void sleepWithoutTry(int milliseconds) throws InterruptedException {
    Thread.sleep(milliseconds);
}

In this case, there’s no need for a try-catch block because the method announces that it has a risk of throwing an InterruptedException. Of course, any code that uses this method will have to have a try-catch block or specify that it also throws InterruptedException. A method can throw many different exceptions, and you can simply list them out after the throws keyword, separated by commas.

Almost every exception thrown in Java is a child class of Exception, RuntimeException, or Error. Any descendant of RuntimeException or Error is an unchecked exception and is exempt from the catch or specify requirement. Any direct descendant of Exception is a checked exception and must either be caught with a try-catch block or specified with the throws keyword. We say direct descendant because RuntimeException is a child of Exception, leading to the confusing situation where only those descendants of Exception which are not also descendants of RuntimeException are checked.

12.3.4. The finally keyword

To deal with the situation in which an important cleanup or finalizing task must be done no matter what, the designers of Java introduced the finally keyword. A finally block comes after all the catch blocks following a try block. The code inside the finally block will be executed whether or not any exception was thrown. A finally block is often used with file I/O to close the file, which should be closed whether or not something went wrong in the process of reading it, as we’ll demonstrate in Section 20.3.2.

The finally keyword is unusually powerful. If an exception isn’t caught and propagates up another level, the finally block will be executed before propagating the exception. Even a return statement will wait for a finally block to be executed before returning, leading to the following bizarre possibility.

public static boolean neverTrue() {
    try {
        return true;
    }
    finally {
        return false;
    }
}

This method attempts to return true, but before it can finish, the finally block returns false. Only one value can be returned, and the finally block wins. You should be aware of finally blocks and their unusual semantics. Use them sparingly and only for careful cleanup operations when needed to guarantee that some event occurs.

Code in a finally block will execute no matter what unless the JVM exits or the thread in question terminates.

12.3.5. Customized exceptions

Exceptions are most useful when dealing with problems encountered by API code. In those cases, your code must merely catch exceptions defined by someone else; however, it’s sometimes useful to define your own exceptions. For one thing, you might write some API code yourself. Generally, you’ll want to use the standard exceptions whenever possible, but your code might generate some unusual or specific error condition that you want to communicate to a programmer, using your own exception.

Defining a new exception is surprisingly simple. All you have to do is write a class that extends Exception. Theoretically, you could extend RuntimeException or Error instead, but you typically won’t. Children of RuntimeException are intended to indicate a bug in the program, and children of Error are intended to indicate a system error.

When creating your new exception, you don’t even have to create any methods, but it’s wise to implement a default constructor and one that takes a String as an additional message.

public class EndOfWorldException extends Exception {
    public EndOfWorldException() {}

    public EndOfWorldException(String message) {
        super(message);
    }
}

As with all other classes, your exceptions should be named in a readable way. This exception is apparently thrown when the world ends. It’s considered good style to end the name of any exception class with Exception. An exception class is a fully fledged class. If you need to add other fields or methods to give your exception the functionality it needs, go ahead. However, the main value of an exception lies in its existence as a named error, not in any tricks it can perform.

Here we’ll give a few examples of exception handling, although exceptions are more useful in large systems with heavy API use. We’ll start with an example of a simple calculator that detects division by zero, then look at exceptions as a tool to detect array bounds problems, and end with a custom exception used with the Color class.

Example 12.1 Calculator with division by zero

Here we implement a quick calculator that reads input from the user in the form of integer operator integer, where operator is one of the four basic arithmetic operators (+, -, *, /). Our code will perform the appropriate operation and output the answer, but we’ll use exception handling to avoid killing the program when a division by zero occurs.

import java.util.*;

public class QuickCalculator {
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);        
        int answer = 0;
        String line = in.nextLine().trim().toLowerCase(); (1)
        while(!line.equals("quit")) { (2)
            String[] terms = line.split(" "); (3)
            int a = Integer.parseInt(terms[0]);
            char operator = terms[1].charAt(0);
            int b = Integer.parseInt(terms[2]);
1 The program reads a line of input from the user.
2 It tests to see if it’s the sentinel value "quit".
3 If it isn’t, the program parses it into two int values and a char.
            try{ (1)
                switch(operator) { (2)
                    case '+': answer = a + b; break;
                    case '-': answer = a - b; break;
                    case '*': answer = a * b; break;
                    case '/': answer = a / b; break;
                }
                System.out.println("Answer: " + answer); (3)
            }
            catch(ArithmeticException e) { (4)
                System.out.println("You can't divide by 0!");
            }
            line = in.nextLine().trim().toLowerCase();
        }
    }
}
1 Here we have a try block enclosing the code where the operations occur.
2 Inside the switch statement, the code blindly performs addition, subtraction, multiplication, or division, depending on the value of operator.
3 Then, it prints the answer.
4 However, if a division by zero occurs, the execution jumps to the catch block and prints an appropriate message.

This try-catch pair is situated inside the loop so that the input will continue even if there was a division by zero. We could achieve the same effect by using an if statement to test if the divisor is zero, but our solution allows easy extensions if there are other exceptions we want to catch.

Example 12.2 Array bounds

Exceptions provide a lot of power. If we want, we can use the ArrayOutOfBoundsException as a crutch when we don’t want to think about the bounds of our array. Although this makes for an interesting example, exceptions should not be used in Java to perform normal tasks. This method takes in an array of int values and prints them all out.

public static void exceptionalArrayPrint(int[] array) {
    try {
        int i = 0;
        while(true)
            System.out.print(array[i++] + " ");
    }
    catch(ArrayIndexOutOfBoundsException e) {}
}

Although the while loop will run without stopping, the moment that i reaches array.length, it will throw an ArrayIndexOutOfBoundsException when it tries to access that element in array. Since we left the catch block empty, nothing will happen, the method will return, and everything will work fine. This example is a peculiar kind of laziness, indeed, since a for loop could achieve the same effect with fewer lines of code.

Programmers can be tempted to abuse exceptions in this way when a lot of calculations are needed to determine the correct bounds. Consider a game of Connect Four. To see if a player has won, the computer must examine all horizontal, vertical, and diagonal possibilities for four in a row. If the game board is represented as a 2D array, the programmer must be careful to make sure that checking for four in a row does not access any index greater than the last row or column or smaller than 0.

The danger of using exceptions for these kinds of tasks has several sources. First, the programmer may not deeply understand the problem and may be careless about the solution. Second, there’s a risk of hiding exceptions that are generated because of real errors. Third, the code becomes difficult to read and unintuitive. Finally, excessive use of exceptions can negatively impact performance.

Example 12.3 Color ranges

The Color class provided by Java allows us to represent a color as a triple of red, green, and blue values with each value in the range [0,255]. Using these three components, we can produce 2563 = 16,777,216 colors. If we were programming some image manipulation software, we might want to be able to increase the red, green, or blue values separately. If changing a value makes it larger than 255, we could throw an exception. Likewise, if changing a value makes it less than 0, we could throw a different exception. Let’s give two custom exceptions that could serve in these roles.

public class ColorUnderflowException extends Exception {
    public ColorUnderflowException(String message) {
        super(message);
    }
    public ColorUnderflowException() { super(); }
}
public class ColorOverflowException extends Exception {
    public ColorOverflowException(String message) {
        super(message);
    }
    public ColorOverflowException() { super(); }
}

Now we can write six methods, each of which increases or decreases the red, green, or blue component of a Color object by 5. If the value of the component is out of range, an appropriate exception will be thrown.

public static Color increaseRed(Color color)
    throws ColorOverflowException {
    if(color.getRed() + 5 > 255)
        throw new ColorOverflowException("Red: " + (color.getRed() + 5));
    else
        return new Color(color.getRed() + 5, color.getGreen(), color.getBlue());
}

public static Color increaseGreen(Color color)
    throws ColorOverflowException {
    if(color.getGreen() + 5 > 255)
        throw new ColorOverflowException("Green: " + (color.getGreen() + 5));
    else
        return new Color(color.getRed(), color.getGreen() + 5, color.getBlue());
}

public static Color increaseBlue(Color color)
    throws ColorOverflowException {
    if(color.getBlue() + 5 > 255)
        throw new ColorOverflowException("Blue: " + (color.getBlue() + 5));
    else
        return new Color(color.getRed(), color.getGreen(), color.getBlue() + 5);
}

public static Color decreaseRed(Color color) throws ColorUnderflowException {
    if(color.getRed() - 5 < 0)
        throw new ColorUnderflowException("Red: " + (color.getRed() - 5));
    else
        return new Color(color.getRed() - 5, color.getGreen(), color.getBlue());
}

public static Color decreaseGreen(Color color)
    throws ColorUnderflowException {
    if(color.getGreen() - 5 < 0)
        throw new ColorUnderflowException("Green: " + (color.getGreen() - 5));
    else
        return new Color(color.getRed(), color.getGreen() - 5, color.getBlue());
}

public static Color decreaseBlue(Color color)
    throws ColorUnderflowException {
    if(color.getBlue() - 5 < 0)
        throw new ColorUnderflowException("Blue: " + (color.getBlue() - 5));
    else
        return new Color(color.getRed(), color.getGreen(), color.getBlue() - 5);
}

Finally, we can write a short method that changes a given color based on user input and deals with exceptions appropriately.

public static Color changeColor(Color color) {
    System.out.println("Enter 'R', 'G', or 'B' to increase " +
        "the amount of red, green, or blue in your color. " +
        "Enter 'r', 'g', or 'b' to decrease the amount of " +
        "red, green, or blue in your color.");
        Scanner in = new Scanner(System.in);
    try {
        switch(in.next().trim().charAt(0)) {
            case 'R': color = increaseRed(color); break;
            case 'G': color = increaseGreen(color); break;
            case 'B': color = increaseBlue(color); break;
            case 'r': color = decreaseRed(color); break;
            case 'g': color = decreaseGreen(color); break;
            case 'b': color = decreaseBlue(color); break;
        }
    }
    catch(ColorOverflowException e) {
        System.out.println(e);
    }
    catch(ColorUnderflowException e) {
        System.out.println(e);
    }
    return color;
}

The code that uses these methods and exceptions is compact. One try block enclosing the method calls is needed so that the exceptions can be caught. Following the try, there’s a catch block for the ColorOverflowException and one for the ColorUnderflowException. Each will print out its exception, including the customized message inside. If an exception occurred, the value of color would remain unchanged because the execution would have jumped to a catch block before the assignment could happen.

Note that when catching the ColorOverflowException or the ColorUnderflowException in the above code, we do the same thing in either case: print the exception. To reduce the amount of code, we could have caught Exception instead of those two specific exceptions, but doing so would catch all exceptions, not just the two related to color that we care about.

In Java 7 and higher, there’s special syntax to deal with situations like the one above where several specific exceptions are handled in the same way. The exception types are listed in a catch block with pipe symbols (|) between them. There’s still only a single reference to the exception (often named e). Using this updated syntax, we could have written the try-catch above as follows.

    try {
        switch(in.next().trim().charAt(0)) {
            case 'R': color = increaseRed(color); break;
            case 'G': color = increaseGreen(color); break;
            case 'B': color = increaseBlue(color); break;
            case 'r': color = decreaseRed(color); break;
            case 'g': color = decreaseGreen(color); break;
            case 'b': color = decreaseBlue(color); break;
        }
    }
    catch(ColorOverflowException | ColorUnderflowException e) {
        System.out.println(e);
    }

12.4. Solution: Bank burglary

Here’s our solution to the bank burglary problem. Although somewhat fanciful, the process could be expanded into a more serious simulation. We begin by defining each of the exceptions.

public class BurglarAlarmException extends Exception {
    public BurglarAlarmException(String message) {
        super(message);
    }
    public BurglarAlarmException() { super(); }
}
public class WatchmanException extends Exception {
    public WatchmanException(String message) {
        super(message);
    }
    public WatchmanException() { super(); }
}
public class LockPickFailException extends Exception {
    public LockPickFailException(String message) {
        super(message);
    }
    public LockPickFailException() { super(); }
}
public class LootTooHeavyException extends Exception {
    public LootTooHeavyException(String message) {
        super(message);
    }
    public LootTooHeavyException() { super(); }
}

Note that the default constructor for each exception is necessary, since constructors taking a String value are provided for each class. Although these default constructors do nothing other than call their parent constructor, they are needed so that it is possible to create each of these constructors without a customized message.

With the exceptions defined, we can assume that the Bank class and the Vault class throw the appropriate exceptions when something goes wrong. Thus, we can make a Henchman class who can try to do the heist and react appropriately if there’s a problem.

public class Henchman {
    public void burgle(Bank bank) { (1)
        try {
            bank.disableAlarm(); (2)
            bank.breakIn();
            Vault vault = bank.findVault();

            vault.open();
            Loot loot = vault.getLoot();
            loot.carryAway();
            
            System.out.println("We got " + loot + "!"); (3)
        }
1 To burgle a bank, one must create a Henchman object then pass a Bank object into its burgle() method.
2 The method will try to disable the alarm, break into the bank, find the vault, open the vault, get the loot out of the vault, and carry it away.
3 If all those steps happen successfully, the method will print out a String version of the loot.

All of this code is inside of a try block. If an exception is thrown at any point, the following catch blocks will deal with it.

        catch(BurglarAlarmException e) { (1)
            System.out.println("I set off the burglar because " + e.getMessage());
            System.out.println("I had to run away.");
        }
        catch(WatchmanException e) {     (2)
            System.out.println("A watchman caught me because " + e.getMessage());
            System.out.println("Please bail me out of jail.");
        }
        catch(LockPickFailException e) { (3)
            System.out.println("I couldn't pick the vault lock.");
            System.out.println("No loot for us.");
        }
        catch(LootTooHeavyException e) { (4)
            System.out.println("The loot was too heavy to carry.");
            System.out.println("No loot for us.");
        }
        catch(NullPointerException e) {  (5)
            System.out.println("The vault was hidden or empty.");
            System.out.println("No loot for us.");
        }
    }
}
1 If a BurglarAlarmException happens, the henchman is forced to run away.
2 If a WatchmanException happens, the henchman is caught and must be bailed out of jail.
3 If a LockPickFailException the henchman is unable to carry the loot off.
4 Something similar happens for a LootTooHeavyException.
5 The last catch block is a little unusual. In this case, a NullPointerException has occurred. Within the try block, two obvious sources of this exception are the vault and the loot variables. If either of them were null, in the case of a vault that could not be found or a vault that was empty, trying to call a method on that null reference would throw a NullPointerException. Although this code shows the power of exception handling, it’s a little unwieldy since we don’t know which variable was null. Also, it’ll hide any NullPointerException that might happen for other reasons. A better solution would be to check for each of these null cases or create more specific exceptions thrown by findVault() and getLoot() if either returns null.

12.5. Concurrency: Exceptions

Any thread in Java can throw an exception. That thread might be the main thread or it might be an extra one that you spawned yourself. (Or even one spawned behind the scenes through a library call.)

What happens when a thread throws an exception? As we’ve been discussing in this chapter, the exception will either be caught or propagate back to its caller. If the exception is caught, the catch block determines what happens. If the exception propagates back and back and back and is never caught, then what? If you’ve coded some of the examples in this chapter, you might think the entire program crashes, but only the thread throwing the exception dies.

Example 12.4 Multiple threads with exceptions

In a program with a single thread, an exception thrown by the main() method will crash the program, completely halting execution. In a multi-threaded program, execution will continue on all threads that have not thrown exceptions. If even a single thread is executing, the program will run to completion before the JVM shuts down.

Program 12.1 Spawns 10 threads. 9 out of 10 spawned threads as well as the main thread throw an exception and die. The remaining thread outputs the sum of the sines of 1 through 1,000,000.
public class CrazyThread extends Thread {
    private int value;
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++)
            new CrazyThread(i).start();
        throw new RuntimeException();
    }
    
    public CrazyThread(int value) {
        this.value = value;
    }
    
    public void run() {
        if(value == 7) {
            double sum = 0;
            for(int i = 1; i <= 1000000; i++)
                sum += Math.sin(i);
            System.out.println("Sum: " + sum);
        }
        else
            throw new RuntimeException();   
    }
}

In the program given above, all of the threads except one will die because of the RuntimeException that they throw. Note that we use the unchecked RuntimeException so that Java does not complain about the lack of catch blocks. The thread with a value of 7 will complete its calculation and print it to the screen even though the main thread has died. For more information on how to spawn threads, refer to Chapter 13.

This behavior can cause a program that never seems to finish. You might write a program that spawns a number of threads and does some work. Even if the main() method has completed and all the important data has been output, the program won’t terminate if any threads are still alive. This problem can also be caused by creating a GUI (such as a JFrame), which spawns one or more threads indirectly, if the GUI isn’t properly disposed.

12.5.1. InterruptedException

In conjunction with concurrency, one exception deserves special attention: InterruptedException. This exception can happen when a thread calls wait(), join(), or sleep(). It’s a checked exception, requiring either a catch block or a throws specification.

This exception is used in cases where the executing thread must wait for some event to occur or some time to pass. In extreme circumstances, another thread can interrupt the waiting thread, forcing it to continue executing before it’s done waiting. If that happens, the code in the catch block determines how the thread should recover from being awoken prematurely.

Programmers who are new to concurrency in Java are often confused or annoyed by InterruptedException, particularly since it never seems to be thrown. Although it’s thrown rarely, situations such as a system shutting down may be best dealt with by calling interrupt() on a waiting thread, causing such an exception. Although we’ll generally leave the InterruptedException catch block empty in this book, threads written for production code should always handle interruptions gracefully.

12.6. Exercises

Conceptual Problems

  1. What are the advantages of using exceptions instead of returning error codes?

  2. The keywords final and finally, as well as the Object method finalize(), are sometimes confused. What’s the purpose of each one?

  3. What’s the difference between the throw keyword and the throws keyword?

  4. Explain the catch or specify requirement of Java.

  5. What must be done differently when using methods that throw checked exceptions as compared to unchecked exceptions? How do the classes Exception, RuntimeException, and Error play a role?

  6. For every program you write, you could choose to put the entire body of your main() method in a large try block with a catch block at the end that catches Exception. In this way, no exception would cause your program to crash. Why is this approach a bad programming decision?

  7. Why did the designers of Java choose to make NullPointerException and ArithmeticException unchecked exceptions even though a program that unintentionally dereferences a null pointer or divides by zero will often crash?

  8. Consider the following two classes.

    public class Trouble {
        public makeTrouble() {
            throw new ArithmeticException();
        }
    }
    
    public class Hazard {
        public makeHazard() {
            throw new InterruptedException();
        }
    }

    Class Trouble will compile, but class Hazard will not. Explain why and what could be done to make Hazard compile.

  9. What value will the following method always return and why?

    public static int magic(String value) {
        try {
            int x = Integer.parseInt(value);
            return x;
        }
        catch(Exception e) {
            System.out.println("Some exception occurred.");
            return 0;
        }
        finally {
            return -1;
        }
    }
  10. Why will the following segment of code fail to compile?

    try{
        Thread.sleep(1000);
    }
    catch(Exception e) {
        System.out.println("Exception occurred!");
    }
    catch(InterruptedException e) {
        System.out.println("Woke up early!");
    }
  11. Consider the following fragment of Java.

    try {
        throw new NullPointerException();
    }
    finally {
        throw new ArrayIndexOutOfBoundsException();
    }

    This code is legal Java. It’s possible to have a finally block after a try block without any catch blocks between them. However, only a single exception can be active at once. Which exception will propagate up from this code and why?

Programming Practice

  1. The NumberFormatException exception is thrown whenever the Integer.parseInt() method receives a poorly formatted String representation of an integer. Re-implement QuickCalculator to catch any NumberFormatException and give an appropriate message to the user.

  2. Refer to Exercise 11.14 and add to the basic mechanics of the simulation by designing two custom exceptions, CollisionException and LightSpeedException. These exceptions should be thrown, respectively, if two bodies collide or if the total magnitude of a body’s velocity exceeds the speed of light.

  3. Users often log onto systems by entering their user name and a password. Unfortunately, human beings are notoriously bad at picking passwords. In computer security, a tool called a proactive password checker allows a user to pick a password but rejects the choice if it doesn’t meet certain criteria.

    Common criteria for a password are that it must be at least a certain length, must contain must contain uppercase and lowercase letters, must contain numerical digits, must contain symbols, cannot be the same as a list of words from a dictionary, and others.

    Write a short program with a check() method that takes a single String parameter giving a possible password. This method should throw an exception if the password does not meet the matching criteria listed below.

    Password criteria Exception

    At least 8 characters in length

    TooShortException

    Contains both upper- and lowercase letters

    NoMixedCaseException

    Contains at least one numerical digit

    NoDigitException

    Contains at least one symbol

    NoSymbolException

    Your main() method should prompt the user to select a password and then pass it to the check() method. If the method throws an exception, you should catch it and print an appropriate error message. Otherwise, you should report to the user that the password is acceptable. Note that you’ll need to define each of the four exceptions as well.

Experiments

  1. Throwing and catching exceptions is a useful tool for making robust programs in Java. However, the JVM machinery needed to implement such a powerful tool is complex. Create an array containing 100,000 random int values. First, sum all these variables up using a for loop and time how long it takes. Then, do the same thing, but, inside of the for loop, put a try block containing a simple division by zero instruction such as x = 5 / 0;. After the try block, put a catch block catching an ArithmeticException. Time this version of the code. Again, you may wish to use System.nanoTime() to measure the time accurately. Was there a large difference in the time taken? Do your findings have any implications for code that routinely throws thousands of exceptions?

13. Concurrent Programming

Time is the substance from which I am made. Time is a river which carries me along, but I am the river; it is a tiger that devours me, but I am the tiger; it is a fire that consumes me, but I am the fire.

— Jorge Luis Borges

13.1. Introduction

So far we’ve focused mostly on writing sequential programs. Sequential execution means that program statements are executed one at a time in a sequence determined by program logic and input data. However, more than one program statement can be executed independently by a multicore processor. While it’s common for programmers to write sequential programs, the widespread availability of multicore processors in a single computer has led to an increase in the demand for programmers who can write concurrent programs.

A concurrent program is one in which several statements can be executed simultaneously by two or more cores. In this chapter we show how to write simple concurrent programs in Java that exploit the power of a multicore computer. We begin with a problem where the fate of the planet’s in grave danger!

13.2. Problem: Deadly virus

A deadly virus capable of wiping out the world’s population is about to be released by an evil mastermind. Only he knows the security code that can stop the countdown. The world is doomed. The single hope for salvation lies with you and your Java programming skills. Through the investigations of a top secret, government-funded spy ring, it’s been revealed that the security code is tied to the number 59,984,005,171,248,659. This large number is the product of two prime numbers, and the security code is their sum. All you need to do is factor the number 59,984,005,171,248,659 into its two prime factors and add them.

Of course, there’s a catch. The deadly virus is going to be released soon, so soon that there might not be enough time for your computer to search through all the numbers one by one. Instead, you must use concurrency to check more than one number at a time.

Does this problem sound contrived? To keep information sent over the Internet private, many kinds of public key cryptography rely on the difficulty of factoring large numbers. Although factoring the number in this problem isn’t difficult, the numbers used for public key cryptography, typically more than 300 decimal digits long, have resisted factorization by even the fastest computers.

13.3. Concepts: Splitting up work

The deadly virus problem has one large task (factoring a number) to perform. How should we split up this task so that we can do it more quickly? Splitting up the work to be done is at the heart of any concurrent solution to a problem.

In a multicore processor, each core is an independent worker. It takes some care to coordinate these workers. First of all, we still need to get the right answer. A concurrent solution is worthless if it’s incorrect, and by reading and writing to the same shared memory, answers found by one core can corrupt answers found by other cores. Preventing that problem will be addressed in Chapter 14. Once we can guarantee the concurrent solution is correct, we also want to improve performance. Perhaps we want the task to finish more quickly. Perhaps we have an interactive system that should continue to handle user requests even though it’s working on a solution in the background. Again, if the overhead of coordinating our workers takes more time than a sequential solution or makes the system less responsive, it’s not useful.

There are two main ways to split up work. The first is called task decomposition. In this approach, each worker is given a different job to do. The second is called domain decomposition. In this approach, the workers do the same job but to different data.

It’s possible to use both task and domain decomposition together to solve the same problem. With both kinds of decomposition, it’s usually necessary to coordinate the workers so that they can share information. In the next two subsections, we describe task decomposition and domain decomposition in greater depth. Then we discuss mapping tasks to threads of execution and the different memory architectures that can be used for concurrent programming.

13.3.1. Task decomposition

The idea of breaking a task down into smaller subtasks is a natural one. Imagine you’re planning a dinner party. You need to buy supplies, cook the dinner, clean your house, and set the table. If four of you were planning the party, each could perform a separate activity. The preparations could go much faster than if a single person was doing the work, but coordination is still important. Perhaps the person cooking the dinner couldn’t finish until certain supplies were bought.

Task decomposition is often easier than domain decomposition because many tasks have natural divisions. Unfortunately, it’s not always an effective way to use multiple cores on a computer. If one task finishes long before the others, a core might sit idle.

The next two examples give simple illustrations of the process of splitting a task into smaller subtasks in the context of multicore programming.

Example 13.1 Video game tasks

Consider a simple video game that consists of the following tasks.

  1. Start game

  2. Process move

  3. Update score

  4. Repaint screen

  5. End game

video game tasks
Figure 13.1 Execution of tasks in a video game. (a) Sequential execution on a single core. (b) Concurrent execution on two cores. Arrows show the flow of execution and data transfer.

Suppose that tasks B and D are independent and can be executed concurrently if two cores are available. Task D continually updates the screen with the old data until task C updates the information.

Figure 13.1(a) and (b) show how the tasks in this video game can be sequenced, respectively, on a single core or on two cores. All tasks are executed sequentially on a single-core processor. In a dual-core processor tasks B and C can execute on one core while task D is executing concurrently on another. Note from the figure that task C sends the score and any other data to task D which is continuously updating the screen. Having two cores can allow a faster refresh of the display since the processor doesn’t have to wait for tasks B or C to complete.

Example 13.2 Math expression tasks

Suppose we need to evaluate the mathematical expression 2Kate-⁠at² with parameters a and K at a given value of t. We can divide the expression into two terms: 2Kat and e-⁠at². Each of these terms can be assigned to a different task for evaluation. On a dual-core processor, these two tasks can be executed on separate cores and the results from each combined to find the value of the expression for the main task.

mathematical expression evaluation
Figure 13.2 Evaluation of a mathematical expression (a) sequentially on a single core and (b) concurrently on two cores. Arrows show the flow of execution and data transfer. Bold typeface indicates the operation being performed.

Figure 13.2 shows how this expression can be evaluated on single core and dual-core processors. Sometimes, using multiple cores to evaluate an expression like this will take less time than a single core. However, there’s no guarantee that using multiple cores will always be faster since tasks take time to set up and to communicate with each other.

These examples illustrate how a task can be divided into two or more subtasks executed by different cores of a processor. We use a dual-core processor for our examples, but the same ideas can be expanded to a larger number of cores.

13.3.2. Domain decomposition

In a computer program, every task performs operations on data. This data is called the domain of that task. In domain decomposition, the data is divided into smaller chunks where each chunk is assigned to a different core, instead of dividing a task into subtasks. Thus, each core executes the same task but on different data.

In the example of the dinner party, we could have used domain decomposition instead of (or in addition to) task decomposition. If you want to cook a massive batch of mashed potatoes, you could peel 24 potatoes yourself. However, if there are four people (and each has a potato peeler), each person would only need to peel 6 potatoes.

The strategy of domain decomposition is very useful and is one of the major focuses of concurrency in this book. Problems in modern computing often use massive data, comprising millions of values or thousands of database records. Writing programs that can chop up data so that multiple cores can process smaller sections of it can greatly speed up the time it takes to finish computation.

In some ways, domain decomposition can be more difficult than task decomposition. The data must be divided evenly and fairly. Once each section of data has been processed, the results must be combined. Companies like Google that process massive amounts of information have developed terminology to describe this process. Dividing up the data and assigning it to workers is called the map step. Combining the partial answers into the final answer is called the reduce step.

We illustrate the domain decomposition strategy in the next two examples.

Example 13.3 Array summation preview

Suppose we want to apply function f to each element of an array a and sum the results. Mathematically, we want to compute the following sum.

oneCoreSum

In this formula, ai is the ith element of array a. Let’s assume we have a dual-core processor available to compute the sum. We split up the array so that each core performs the task on half of the array. Let S1 and S2 denote the sums computed by core 1 and core 2, respectively.

twoCoreSum

Assuming that N is even, both cores process exactly the same amount of data. For odd N, one of the cores processes one more data item than the other.

arrayDecomposition
Figure 13.3 Computing the sum of a function of each element of an array.

After S1 and S2 have been computed, one of the cores can add these two numbers together to get S. This strategy is illustrated in Figure 13.3. After the two cores have completed their work on each half of the array, the individual sums are added together to produce the final sum.

Example 13.4 Matrix multiplication preview

The need to multiply matrices arises in many mathematical, scientific, and engineering applications. Suppose we’re asked to write a program to multiply two square matrices A and B, which are both n × n matrices. The product matrix C will also be n × n. A sequential program will compute each element of matrix C one at a time. However, a concurrent program can compute more than one element of C simultaneously using multiple cores.

matrixDecomposition
Figure 13.4 Data decomposition to multiply two 4 × 4 matrices. The two cores perform the same multiplication tasks but on different data from matrix A. The two cores compute the top two and bottom two rows of C, respectively.

In this problem, the task is to multiply matrices A and B. Through domain decomposition, we can replicate this task on each core. As shown in Figure 13.4, each core computes only a portion of C. For example, if A and B are 4 × 4 matrices, we can ask one core to compute the product of the first two rows of A with all four columns of B to generate the first two rows of C. The second core computes the remaining two rows of C. Both cores can access matrices A and B.

13.3.3. Tasks and threads

It’s the programmer’s responsibility to divide his or her solution into a number of tasks and subtasks which will run on one or more cores on a processor. In previous sections, we described concurrent programs as if specific tasks could be assigned specific cores, but Java doesn’t provide a direct way to do so.

Instead, a Java programmer must group together a set of tasks and subtasks into a thread. A thread is very much like a sequential program. In fact, all sequential programs are made up of a single thread. A thread is a segment of executing code that runs through its instructions step by step. Each thread can run independently. If you have a single core processor, only one thread can run at a time, and all the threads will take turns. If you have a multicore processor, as many threads as there are cores can execute at the same time. You can’t pick which core a given thread will run on. In most cases, you won’t even be able to tell which core a given thread is using.

It takes care to package up the right set of tasks into a single thread of execution. Recall the previous examples of concurrent programming in this chapter.

Consider dividing the tasks in Example 13.1 into two threads. Tasks B and C in can be packaged into thread 1, and task D can be packaged into thread 2. This division is shown in Figure 13.5(a).

Tasks to evaluate different subexpressions in Example 13.2 can also be divided into two threads as shown in Figure 13.5(b). In many problems there are several reasonable ways of dividing a set of subtasks into threads.

task thread packaging
Figure 13.5 (a) Tasks in a video game shown packaged into two threads. (b) Tasks to evaluate a mathematical expression shown packaged into two threads. Each thread may or may not run on the same core as the other.

Note that these figures look exactly like the earlier figures, except that the tasks are grouped as threads instead of cores. This grouping matches reality better, since we can control how the tasks are packaged into threads but not how they are assigned to cores.

In both examples, we have two threads. It’s possible that some other thread started these threads running. Every Java program, concurrent or sequential, starts with one thread. We’ll refer to this thread as the main thread since it contains the main() method.

Example 13.3 and Example 13.4 use multiple identical tasks, but these tasks operate on different data. In Example 13.3, the two tasks can be assigned to two threads that operate on different portions of the input array. The task of summing the results from the two threads can either be a separate thread or a subtask included in one of the other threads. In Example 13.4, the two tasks can again be assigned to two distinct threads that operate on different parts of the input matrix A to generate the corresponding portions of the output matrix C.

There can be many ways to package tasks into threads. There can also be many ways to decompose data into smaller chunks. The best ways to perform these subdivisions of tasks or data depend on the problem at hand and the processor architecture on which the program will be executed.

13.3.4. Memory architectures and concurrency

The two most important paradigms for concurrent programming are message passing and shared memory systems. Each paradigm handles communication between the various units of code running in parallel in a different way. Message passing systems such as MPI approach this problem by sending messages between otherwise independent units of code called processes. A process which is executing a task may have to wait until it receives a message from another process before it knows how to proceed. Messages can be sent from a single process to one other or broadcast to many. Message passing systems are especially useful when the processors doing the work do not share memory.

In contrast, the built-in system for concurrency in Java uses the shared memory paradigm. In Java, a programmer can create a number of threads which share the same memory space. Each thread is an object which can perform work. We described threads as a way to package up a group of tasks, and processes are another. People use the term processes to describe units of code executing with separate memory and threads to describe units of code executing with shared memory.

When you first learned to program, one of the biggest challenges was probably learning to solve a problem step by step. Each line of the program had to be executed one at a time, logically and deterministically. Human beings don’t naturally think that way. We tend to jump from one thing to another, making inferences and guesses, thinking about two unrelated things at once, and so on. As you know now, it’s only possible to write and debug programs because of the methodical way they work.

You can imagine the execution of a program as an arrow that points to one line of code, then the next, then the next, and so on. We can think of the movement of this arrow as the thread of execution of the program. The code does the actual work, but the arrow keeps track of where execution in the program currently is. The code can move the arrow forward, it can do basic arithmetic, it can decide between choices with if statements, it can do things repeatedly with loops, it can jump into a method and then come back. A single thread of execution can do all of these things, but its arrow can’t be two places at once. It can’t be dividing two numbers in one part of the program and evaluating an if statement in another. However, there’s a way to split this thread of execution so that two or more threads are executing different parts of the program, and the next section will show you how this is done in Java.

13.4. Syntax: Threads in Java

13.4.1. The Thread class

Java, like many programming languages, includes the necessary features to package tasks and subtasks into threads. The Thread class and its subclasses provide the tools for creating and managing threads. For example, the following class definition allows objects of type ThreadedTask to be created. Such an object can be executed as a separate thread.

public class ThreadedTask extends Thread {
    // Add constructor and body of class
}

The constructor is written just like any other constructor, but there’s a special run() method in Thread that can be overridden by any of its subclasses. This method is the starting point for the thread of execution associated with an instance of the class. Most Java applications begin with a single main thread which starts in a main() method. Additional threads must start somewhere, and that place is the run() method. A Java application will continue to run as long as at least one thread is active. The following example shows two threads, each evaluating a separate subexpression as in Figure 13.5(b).

Example 13.5 Thread samples

We’ll create Thread1 and Thread2 classes. The threads of execution created by instances of these classes compute, respectively, the two subexpressions in Figure 13.5(b) and save the computed values.

public class Thread1 extends Thread {
    private double K, a, t, value;
 
    public Thread1(double K, double a, double t) {
        this.K = K;
        this.a = a;
        this.t = t;
    } 
    public void run() { value = 2*K*a*t; }
    public double getValue() { return value; }
}
public class Thread2 extends Thread {
    private double a, t, value;
    
    public  Thread2(double a, double t) {
        this.a = a;
        this.t = t;
    }
    public void run() { value = Math.exp(-a*t*t); }
    public double getValue() { return value; }
}

The run() method in each thread above computes a subexpression and saves its value. We show how these threads can be executed to solve the math expression problem in Example 13.6.

13.4.2. Creating a thread object

Creating an object from a subclass of Thread is the same as creating any other object in Java. For example, we can instantiate the Thread1 class above to create an object called thread1.

Thread1 thread1 = new Thread1(15.1, 2.8, 7.53);

Using the new keyword to invoke the constructor creates a Thread1 object, but it doesn’t start executing it as a new thread. As with all other classes, the constructor initializes the values inside of the new object. A subclass of Thread can have many different constructors with whatever parameters its designer thinks appropriate.

13.4.3. Starting a thread

To start the thread object executing, its start() method must be called. For example, the thread1 object created above can be started as follows.

thread1.start();

Once started, a thread executes independently. Calling the start() method automatically calls the object’s run() method behind the scenes. When a thread needs to share data with another thread, it might have to wait.

13.4.4. Waiting for a thread

Often some thread, main or otherwise, needs to wait for another thread before proceeding further with its execution. The join() method is used to wait for a thread to finish executing. For example, whichever thread executes the following code will wait for thread1 to complete.

thread1.join();

Calling join() is a blocking call, meaning that the code calling this method will wait until it returns. Since it can throw a a checked InterruptedException while the code’s waiting, the join() method is generally used within a try-catch block. We can add a try-catch block to the thread1 example so that we can recover from being interrupted while waiting for thread1 to finish.

try {
	System.out.println("Waiting for thread 1...");
	thread1.join();
	System.out.println("Thread 1 finished!");
}
catch (InterruptedException e) {
	System.out.println("Thread 1 didn't finish!");
}

Note that the InterruptedException is thrown because the main thread was interrupted while waiting for thread1 to finish. If the join() call returns, then thread1 must have finished, and we inform the user. If an InterruptedException is thrown, some outside thread must have interrupted the main thread, forcing it to stop waiting for thread1.

In earlier versions of Java, there was a stop() method which would stop an executing thread. Although this method still exists, it’s been deprecated and shouldn’t be used because it can make a program behave in an unexpected way.

Example 13.6 Math expression threads

Now that we have the syntax to start threads and wait for them to finish, we can use the threads defined in Example 13.5 with a main thread to make our first complete concurrent program. The main thread in class MathExpression creates and starts the worker threads thread1 and thread2 and waits for their completion. When the two threads complete their execution, we can ask each for its computed value. The main thread then prints the product of these values, which is the result of the expression we want to evaluate.

public class MathExpression { 
    public static void main (String[] args) {
        double K = 120, a = 1.2, t = 2;
        Thread1 thread1 = new Thread1(K, a, t);
        Thread2 thread2 = new Thread2(a, t);
        thread1.start(); // Start thread1
        thread2.start(); // Start thread2
        try { // Wait for both threads to complete
            thread1.join();
            thread2.join();
            System.out.println("Value of expression: " +
                    thread1.getValue()*thread2.getValue());
        }
        catch (InterruptedException e) {
            System.out.println("A thread didn't finish!");
        }        
    }
}

We want to make it absolutely clear when threads are created, start executing, and finish. These details are crucial for the finer points of concurrent Java programming. In Figure 13.5, it appears as if execution of the concurrent math expression evaluation begins with Thread 1 which spawns Thread 2. Although that figure explains the basics of task decomposition well, the details are messier for real Java code.

In the code above, execution starts with the main() method in MathExpression. It creates Thread1 and Thread2 objects and waits for them to finish. Then, it reads the values from the objects after they’ve stopped executing. We could have put the main() method in the Thread1 class, omitting the MathExpression class entirely. Doing so would make the execution match Figure 13.5 more closely, but it would make the two Thread subclasses less symmetrical: The main thread and thread1 would both independently execute code inside the Thread1 class while only thread2 would execute code inside the Thread2 class.

thread lifecycle
Figure 13.6 Creation, starting, and joining of threads in MathExpression, Thread1, and Thread2.

Figure 13.6 shows the execution of thread1 and thread2 and the main thread. Note that the JVM implicitly creates and starts the main thread which explicitly creates and starts thread1 and thread2. Even after the threads associated with thread1 and thread2 have stopped running, the objects associated with them continue to exist. Their methods and fields can still be accessed.

13.4.5. The Runnable interface

Although it’s possible to create Java threads by inheriting from the Thread class directly, the Java API allows the programmer to use an interface instead.

As an example, the Summer class takes an array of int values and sums them up within a given range. If multiple instances of this class are executed as separate threads, each one can sum up different parts of an array.

public class Summer implements Runnable {
    int[] array;
    int lower;
    int upper;
    int sum = 0;
    
    public Summer(int[] array, int lower, int upper) {
        this.array = array;
        this.lower = lower;
        this.upper = upper;
    }
    
    public void run() {
        for(int i = lower; i < upper; i++)
            sum += array[i];
    }
    
    public int getSum() { return sum; }
}

This class is very similar to one that inherits from Thread. Imagine for a moment that the code following Summer is extends Thread instead of implements Runnable. The key thing a class derived from Thread needs is an overridden run() method. Since only the run() method is important, the designers of Java provided a way to create a thread using the Runnable interface. To implement this interface, only a public void run() method is required.

When creating a new thread, there are some differences in syntax between the two styles. The familiar way of creating and running a thread from a Thread subclass is as follows.

Summer summer = new Summer(array, lower, upper);
summer.start();

Since Summer doesn’t inherit from Thread, it doesn’t have a start() method, and this code won’t compile. When a class only implements Runnable, it’s still necessary to create a Thread object and call its start() method. Thus, an extra step is needed.

Summer summer = new Summer(array, lower, upper);
Thread thread = new Thread(summer);
thread.start();

This alternate way of implementing the Runnable interface seems more cumbersome than inheriting directly from Thread, since you have to instantiate a separate Thread object. However, most developers prefer to design classes that implement Runnable instead of inheriting from Thread. Why? Java only allows for single inheritance. If your class implements Runnable, it’s free to inherit from another parent class with features you might want.

Example 13.7 Array of threads

In domain decomposition, we often need to create multiple threads, all from the same class. As an example, consider the following thread declaration.

public class NumberedThread extends Thread {
    private int value;

    public NumberedThread(int input) { value = input; }
    
    public void run() {
        System.out.println("Thread " + value);
    }
}

Now, suppose we want to create 10 thread objects of type NumberedThread, start them, and then wait for them to complete.

NumberedThread[] threads = new NumberedThread[10]; (1)
for(int i = 0; i < threads.length; i++) {
    threads[i] = new NumberedThread(i); (2)
    threads[i].start(); (3)
}
try {
    for(int i = 0; i < threads.length; i++)
        threads[i].join(); (4)
}
catch(InterruptedException e) {
    System.out.println("A thread didn't finish!");
}
1 First, we declare an array to hold references to NumberedThread objects. Like any other type, we can make an array to hold objects that inherit from Thread.
2 The first line of the for loop instantiates a new NumberedThread object, invoking the constructor which stores the current iteration of the loop into the value field. The reference to each NumberedThread object is stored in the array. Remember that the constructor does not start a new thread running.
3 The second line of the for loop does that.
4 We’re also interested in when the threads stop. Calling the join() method forces the main thread to wait for each thread to finish.

The entire second for loop is nested inside of a try block. If the main thread is interrupted while waiting for any of the threads to finish, an InterruptedException will be caught. As before, we warn the user that a thread didn’t finish. For production-quality code, the catch block should handle the exception in such a way that the thread can recover and do useful work even though it didn’t get what it was waiting for.

13.5. Examples: Concurrency and speedup

Speedup is one of the strongest motivations for writing concurrent programs. To understand speedup, let’s assume we have a problem to solve. We write two programs to solve this problem, one that’s sequential and another that’s concurrent and, hence, able to exploit multiple cores. Let ts be the average time to execute the sequential program and tc the average time to execute the concurrent program. So that the comparison is meaningful, assume that both programs are executed on the same computer. The speedup obtained from concurrent programming is defined as ts/tc.

Speedup measures how much faster the concurrent program executes relative to the sequential program. Ideally, we expect tc < ts, making the speedup greater than 1. However, simply writing a concurrent program doesn’t guarantee that it’s faster than the sequential version.

To determine speedup, we need to measure ts and tc. Time in a Java program can easily be measured with the following two static methods in the System class.

public static long currentTimeMillis()
public static long nanoTime()

The first of these methods returns the current time in milliseconds (ms). A millisecond is 0.001 seconds. This method gives the difference between the current time on your computer’s clock and midnight of January 1, 1970 Coordinated Universal Time (UTC). This point in time is used for many timing features on many computer platforms and is called the Unix epoch. The other method returns the current time in nanoseconds (ns). A nanosecond is 0.000000001 or 10-9 seconds. This method also gives the difference between the current time and some fixed time, which is system dependent and not necessarily the Unix epoch. The System.nanoTime() method can be used when you want timing precision finer than milliseconds; however, the level of accuracy it returns is again system dependent. The next example show how to use these methods to measure execution time.

Example 13.8 Measuring execution time

Suppose we want to measure the execution time of a piece of Java code. For convenience, we can assume this code is contained in the work() method. The following code snippet measures the time to execute work().

long start = System.currentTimeMillis();
work();
long end = System.currentTimeMillis();
System.out.println("Elapsed time: " + (end - start) + " ms");

The output will give the execution time for work() measured in milliseconds. To get the execution time in nanoseconds, use the System.nanoTime() method instead.

Now that we have the tools to measure execution time, we can measure speedup. The next few examples show the speedup (or lack of it) that we can achieve using a concurrent solution to a few simple problems.

Example 13.9 Math expression speedup

Recall the concurrent program in Example 13.6 to evaluate a simple mathematical expression. This program uses two threads. We executed this multi-threaded program on an iMac computer with an Intel Core 2 Duo running at 2.16 Ghz. The execution time was measured at 1,660,000 nanoseconds. We also wrote a simple sequential program to evaluate the same expression. It took 4,100 nanoseconds to execute this program on the same computer. Plugging in these values for tc and ts, we can find the speedup.

speedup

This speedup is much less than 1. Although this result might be surprising, the concurrent program with two threads executes slower than the sequential program. In this example, the cost of creating, running, and joining threads outweighs the benefits of concurrent calculation on two cores.

Example 13.10 Array summation

In Example 13.3, we introduced the problem of applying a function to every value in an array and then summing the results. Let’s say that we want to apply the sine function to each value. To solve this problem concurrently, we partition the array evenly among a number of threads, using the domain decomposition strategy. Each thread finds the sum of the sines of the values in its part of the array. One factor that determines whether or not we achieve speedup is the complexity of the function, in this case sine, that we apply. Although we may achieve speedup with sine, a simpler function such as doubling the value might not create enough work to justify the overhead of using threads.

We create class SumThread whose run() method sums the sines of those elements of the array in its assigned partition.

import java.util.Random;

public class SumThread extends Thread {
    private double sum = 0.0; (1)
    private int lower;
    private int upper;
	private double[] array; (2)
    public static final int SIZE = 1000000; (3)
    public static final int THREADS = 8;
    
    public SumThread(double[] array, int lower, int upper) { (4)
		this.array = array;
        this.lower = lower;
        this.upper = upper;     
    }
1 First, we set up all the fields that the class will need.
2 Note that every SumThread object will have its own reference to the array of data.
3 We fix the array size at 1,000,000 and the number of threads at 8, but these values could easily be changed or read as input instead.
4 In its constructor, a SumThread takes a reference to the array of data and the lower and upper bounds of its partition. Like most ranges we discuss, the lower bound is inclusive though the upper bound is exclusive.
    public void run() {
        for(int i = lower; i < upper; i++)
            sum += Math.sin(array[i]); (1)
    }

    public double getSum() { return sum; } (2)
1 In the for loop of the run() method, the SumThread finds the sine of each number in its array partition and adds that value to its running sum.
2 The getSum() method is an accessor that allows the running sum to be retrieved.
    public static void main(String[] args) {  
        double[] data = new double[SIZE]; (1)
        Random random = new Random();
        int start = 0;  
        for(int i = 0; i < SIZE; i++) (2)
            data[i] = random.nextDouble();  
        SumThread[] threads = new SumThread[THREADS];
        int quotient = data.length / THREADS;
        int remainder = data.length % THREADS;          
        for(int i = 0; i < THREADS; i++) {
            int work = quotient;
            if(i < remainder)
                work++;
            threads[i] = new SumThread(data, start, start + work); (3)
            threads[i].start(); (4)
            start += work;
        }   
1 The main() method begins by instantiating the array.
2 It fills it with random values.
3 Then, each thread is created by passing in a reference to the array and lower and upper bounds that mark the thread’s partition of the array. If the process using the array length and the number of threads to determine upper and lower bounds doesn’t make sense, refer to Section 6.11 which describes the fair division of work to threads. If the length of the array is not divisible by the number of threads, simple division isn’t enough.
4 After each thread is created, its start() method is called.
        double sum = 0.0; (1)
        try { 
            for(int i = 0; i < THREADS; i++) {
                threads[i].join(); (2)
                sum += threads[i].getSum(); (3)
            }
            System.out.println("Sum: " + threads[0].getSum()); (4)
        }
        catch(InterruptedException e) {
            e.printStackTrace(); (5)
        }
    }   
}
1 Once the threads have started working, the main thread creates its own running total.
2 It iterates through each thread waiting for it to complete.
3 When each thread is done, its value is added to the running total.
4 Finally, the sum is printed out.
5 If the main thread is interrupted while waiting for a thread to complete, the stack trace is printed.
Example 13.11 Matrix multiplication

In Example 13.4, we discussed the importance of matrix operations in many applications. Now that we know the necessary Java syntax, we can write a concurrent program to multiply two square matrices A and B and compute the resultant matrix C. If these matrices have n rows and n columns, the value at the ith row and jth column of C is

matrixValue

In Java, it’s natural for us to store matrices as 2-dimensional arrays. To do this multiplication sequentially, the simplest approach uses three nested for loops. The code below is a direct translation of the mathematical notation, but we do have to be careful about bookkeeping. Note that mathematical notation often uses uppercase letters to represent matrices though the Java convention is to start all variable names with lowercase letters.

for(int i = 0; i < c.length; i++)
    for(int j = 0; j < c[i].length; j++)
        for(int k = 0; k < b.length; k++)
            c[i][j] += a[i][k] * b[k][j];

The first step in making a concurrent solution to this problem is to create a Thread subclass which will do some part of the matrix multiplication. Below is the MatrixThread class which will compute a number of rows in the answer matrix c.

public class MatrixThread extends Thread {
    private double[][] a;
    private double[][] b;
    private double[][] c;
    private int lower;
    private int upper;  
    
    public MatrixThread(double[][] a, double[][] b, (1)
        double[][] c, int lower, int upper) {      
        this.a = a;
        this.b = b;
        this.c = c;
        this.lower = lower;
        this.upper = upper;
    }
    
    public void run() { (2)
        for(int i = lower; i < upper; i++)
            for(int j = 0; j < c[i].length; j++)              
                for(int k = 0; k < b.length; k++)
                    c[i][j] += a[i][k] * b[k][j];
    }
}
1 The constructor for MatrixThread stores references to the arrays corresponding to matrices A, B, and C as well as lower and upper bounds on the rows of C to compute.
2 The body of the run() method is identical to the sequential solution except that its outermost loop runs only from lower to upper instead of through all the rows of the result. It’s critical that each thread is assigned a set of rows that does not overlap with the rows another thread has. Not only would having multiple threads compute the same row be inefficient, it would very likely lead to an incorrect result, as we’ll see in Chapter 14.

The following client code uses an array of MatrixThread objects to perform a matrix multiplication. We assume that an int constant named THREADS has been defined which gives the number of threads we want to create.

MatrixThread[] threads = new MatrixThread[THREADS];
int quotient = c.length / THREADS;
int remainder = c.length % THREADS;
int start = 0;
for(int i = 0; i < THREADS; i++) {
    int rows = quotient;
    if(i < remainder)
        rows++;
    threads[i] = new MatrixThread(a, b, c, start, start + rows); (1)
    threads[i].start(); (2)
    start += rows;
}
try {
    for(int i = 0; i < THREADS; i++) (3)
        threads[i].join();
}
catch(InterruptedException e) {
    e.printStackTrace();
}
1 We loop through the array, creating a MatrixThread object for each location. As in the previous example, we use the approach described in Section 6.11 to allocate rows to each thread fairly. Each new MatrixThread object is given a reference to each of the three matrices as well as an inclusive starting and an exclusive ending row.
2 After the MatrixThread objects are created, we start them running with the next line of code.
3 Next, there’s a familiar for loop with the join() calls that force the main thread to wait for the other threads to finish.

Presumably, code following this snippet will print the values of the result matrix or use it for other calculations. If we didn’t use the join() calls to be sure the threads have finished, we might print out a result matrix that’s only been partially filled in.

We completed the code for threaded matrix multiplication and executed it on an iMac computer with an Intel Core 2 Duo running at 2.16 Ghz. The program was executed for matrices of different sizes (n × n). For each size, the sequential and concurrent execution times in seconds and the corresponding speedup are listed in the following table.

Size (n) ts (s) tc (s) Speedup

100

0.013

0.9

0.014

500

1.75

4.5

0.39

1,000

15.6

10.7

1.45*

Only with 1,000 × 1,000 matrices did we see improved performance when using two threads. In that case, we achieved a speedup of 1.45, marked with an asterisk. In the other two cases, performance became worse.

13.6. Concepts: Thread scheduling

Now that we’ve seen how multiple threads can be used together, a number of questions arise: Who decides when these threads run? How is processor time shared between threads? Can we make any assumptions about the order in which the threads run? Can we affect this order?

These questions focus on thread scheduling. Because different concurrent systems handle scheduling differently, we’ll only describe scheduling in Java. Although sequential programming is all about precise control over what happens next, concurrency takes much of this control away from the programmer. When threads are scheduled and which processor they run on is handled by a combination of the JVM and the OS. With normal JVMs, there’s no explicit way to access the scheduling and alter it to your liking.

Of course, there are a number of implicit ways a programmer can affect scheduling. In Java, as in several other languages and programming systems, threads have priorities. Higher priority threads run more often than lower priority threads. Some threads are performing mission-critical operations which must be carried out as quickly as possible, and some threads are just doing periodic tasks in the background. A programmer can set thread priorities accordingly.

Setting priorities gives only a very general way of controlling which thread will run. The threads themselves might have more specific information about when they will and won’t need processor time. A thread may need to wait for a specific event and won’t need to run until then. Java allows threads to interact with the scheduler through Thread.sleep() and Thread.yield(), which we’ll discuss in Section 13.7, and through the wait(), method which we’ll discuss in Chapter 14.

13.6.1. Nondeterminism

In Java, the mapping of a thread inside the JVM to a thread in the OS varies. Some implementations give each Java thread an OS thread, some put all Java threads on a single OS thread (with the side effect of preventing parallel execution), and some allow for the possibility of changing which OS thread a Java thread uses. Thus, the performance and, in some cases, the correctness of your program might vary, depending on which system you’re running. This is, yet again, one of those times when Java is platform independent…​but not entirely.

Unfortunately, the situation is even more complicated. Making threads part of your program means that the same program could run differently on the same system. The JVM and the OS have to cooperate to schedule threads, and both programs are complex mountains of code which try to balance many factors. If you create three threads, there’s no guarantee that the first will run first, the second second, and the third third, even if it happens that way the first 10 times you run the program. Exercise 13.18 shows that the pattern of thread execution can vary a lot.

In all the programs before this chapter, the same sequence of input would always produce the same sequence of output. Perhaps the biggest hurdle created by this nondeterminism is that programmers must shift their paradigm considerably. The processor can switch between executions of threads at any time, even in the middle of operations. Every possible interleaving of thread execution could crop up at some point. Unless you can be sure that your program behaves properly for all of them, you might never be able to debug your code completely. What’s so insidious about nondeterministic bugs is that they can occur rarely and be almost impossible to reproduce. In this chapter, we’ve introduced how to create and run threads, but making these threads interact properly is a major problem we tackle in subsequent chapters.

After those dire words of warning, we’d like to remind you that nondeterminism is not in itself a bad thing. Many threaded applications with a lot of input and output, such as server applications, necessarily exist in a nondeterministic world. For these programs, many different sequences of thread execution may be perfectly valid. Each individual program may have a different definition of correctness. For example, if a stock market server receives two requests to buy the last share of a particular stock at almost the same time from two threads corresponding to two different clients, it might be correct for either one of them to get that last share. However, it would never be correct for both of them to get it.

13.6.2. Polling

So far the only mechanism we’ve introduced for coordinating different threads is using the join() method to wait for a thread to end. Another technique is polling, or busy waiting. The idea is to keep checking the state of one thread until it changes.

There are a number of problems with this approach. The first is that it wastes CPU cycles. Those cycles spent by the waiting thread continually checking could have been used productively by some other thread in the system. The second problem is that we have to be certain that the state of the thread we’re waiting for won’t change back to the original state or to some other state. Because of the unpredictability of scheduling, there’s no guarantee that the waiting thread will read the state of the other thread when it has the correct value.

We bring up polling partly because it has a historical importance to parallel programming, partly because it can be useful in solving some problems in this chapter, and partly because we want you to understand the reasons why we need better techniques for thread communication.

13.7. Syntax: Thread states

A widely used Java tool for manipulating scheduling is the Thread.sleep() method. This method can be called any time you want a thread to do nothing for a set period of time. Until the sleep timer expires, the thread will not be scheduled for any CPU time, unless it’s interrupted. To make a thread of execution sleep, call Thread.sleep() in that thread of execution with a number of milliseconds as a parameter. For example, calling Thread.sleep(2000) will make the calling thread sleep for two full seconds.

Another useful tool is the Thread.yield() method. It gives up use of the CPU so that the next waiting thread can run. To use it, a thread calls Thread.yield(). This method is useful in practice, but according to official documentation, the JVM doesn’t have to do to anything when a Thread.yield() call happens. The Java specification doesn’t demand a particular implementation. A JVM could ignore a Thread.yield() call completely, but most JVMs will move on to the next thread in the schedule.

thread states
Figure 13.7 Thread states and transitions.

Figure 13.7 shows the lifecycle of a thread. A thread begins its life in the New Thread state, after the constructor is called. When the start() method is called, the thread begins to run and transitions to the Runnable state. Being Runnable doesn’t necessarily mean that the thread is executing at any given moment but that it’s ready to run at any time. When in the Runnable state, a thread may call Thread.yield(), relinquishing use of the processor, but it will still remain Runnable.

However, if a thread goes to sleep with a Thread.sleep() call, waits for a condition to be true using a wait() call, or performs a blocking I/O operation, the thread will transition to the Not Runnable state. Not Runnable threads cannot be scheduled for processor time until they wake up, finish waiting, or complete their I/O. The final state is Terminated. A thread becomes Terminated when its run() method finishes. A Terminated thread cannot become Runnable again and is no longer a separate thread of execution.

Any object with a type that’s a subclass of Thread can tell you its current state using the getState() method. This method returns an enum type, whose value must come from a fixed list of constant objects. These objects are Thread.State.NEW, Thread.State.RUNNABLE, Thread.State.BLOCKED, Thread.State.WAITING, Thread.State.TIMED_WAITING, and Thread.State.TERMINATED. Although the others are self explanatory, we lump the Thread.State.BLOCKED, Thread.State.WAITING, and Thread.State.TIMED_WAITING values into the Not Runnable state, since the distinction between the three isn’t important for us.

Threads also have priorities in Java. When an object that’s a subclass of Thread is created in Java, its priority is initially the same as the thread that creates it. Usually, this priority is Thread.NORM_PRIORITY, but there are some special cases when it’s a good idea to raise or lower this priority. Avoid changing thread priorities because it increases platform dependence and because the effects are not always predictable. Be aware that priorities exist, but don’t use them unless and until you have a good reason.

Example 13.12 Military marching

Let’s apply the ideas discussed above to a lighthearted example. You might be familiar with sound of soldiers marching: “Left, Left, Left, Right, Left!” We can design a thread that prints Left and another thread that prints Right. We can combine the two to print the correct sequence for marching and loop the whole thing 10 times so that we can see how accurately we can place the words. We want to use the scheduling tools discussed above to get the timing right. Let’s try Thread.sleep() first.

public class LeftThread extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.print("Left ");  (1)
            System.out.print("Left ");
            System.out.print("Left ");
            try { Thread.sleep(10); }  (2)
            catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Left"); (3)
        }
    }
}
1 Inside, the for loop, this thread prints out Left three times.
2 The, it waits for 10 milliseconds.
3 Finally, it prints out Left again and repeats the loop.
public class RightThread extends Thread {
    public void run() { 
        try {
			Thread.sleep(5); (1)
			for(int i = 0; i < 10; i++) { 
				System.out.print("Right "); (2)
				Thread.sleep(10); (3)
			}
		}
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1 This thread waits for 5 milliseconds to get synchronized.
2 Inside its for loop, it prints out Right.
3 Then, it waits for 10 milliseconds and repeats the loop.

The driver program below creates a thread for each of these classes and then starts them. If you run this program, you should see 10 lines of Left Left Left Right Left, but there are a few problems.

public class MilitaryMarching {
    public static void main(String[] args) {
        LeftThread left = new LeftThread();
        RightThread right = new RightThread();
        left.start();
        right.start();
        try {
            left.join();
            right.join();
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }       
    }
}

The first problem is that we have to wait some amount of time between calls. We could shorten the Thread.sleep() calls, but there are limits on the resolution of the timer. The bigger problem is that the two threads can sometimes get out of sync. If you run the program many times, you might see a Right out of place once in a while. If you increase the repetitions of the for loops to a larger number, the errors will become more likely. Whether or not you see errors is somewhat system dependent. We can try Thread.yield() instead of Thread.sleep().

public class LeftYieldThread extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.print("Left ");
            System.out.print("Left ");
            System.out.print("Left ");              
            Thread.yield();         
            System.out.println("Left");                 
        }
    }
}
public class RightYieldThread extends Thread {
    public void run() {         
        for(int i = 0; i < 10; i++) { 
            System.out.print("Right ");         
            Thread.yield();
        }
    }
}

These new versions of the two classes have essentially replaced calls to Thread.sleep() with calls to Thread.yield(). Without the need for exception handling, the code is simpler, but we’ve traded one set of problems for another. If there are other threads operating in the same application, they’ll be scheduled in ways that will interfere with the pattern of yielding. If you’re running this code on a machine with a single processor and a single core, you have a good chance of seeing something which matches the expected output. However, if you’re running it on multiple cores, everything will be jumbled. It’s likely that the LeftYieldThread will be running on one processor with the RightYieldThread on another. In that case, neither has any competition to yield to.

Finally, let’s look at a polling solution which still falls short of the mark. To do this, we need state variables inside of each class to keep track of whether or not it’s done. Each thread needs a reference to the other thread to make queries, and the driver program must be updated to add these in before starting the threads.

public class LeftPollingThread extends Thread {
    private RightPollingThread right;
    private boolean done = false;
    
    public void setRight(RightPollingThread right) {
        this.right = right;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.print("Left ");
            System.out.print("Left ");
            System.out.print("Left ");          
            done = true;            
            while(!right.isDone());           
            right.setDone(false);                     
            System.out.println("Left");                 
        }
    }
    
    public boolean isDone() { return done; }    
    public void setDone(boolean value) { done = value; }
}
public class RightPollingThread extends Thread {
    private LeftPollingThread left;
    private boolean done = false;   
    
    public void setLeft(LeftPollingThread left) {
        this.left = left;
    }
    
    public void run() { 
        for(int i = 0; i < 10; i++) {             
            while(!left.isDone());            
            left.setDone(false);            
            System.out.print("Right ");         
            done = true;
        }
    }
    
    public boolean isDone() { return done; }    
    public void setDone(boolean value) { done = value; }
}

Whether single core or multicore, this solution will always give the right output. Or it should. Java experts will point out that we are violating a technicality of the Java Memory Model. Because we’re not using synchronization tools, we have no guarantee that the change of the done variable will even be visible from one thread to another. In practice, this problem should affect you rarely, but to be safe, both of the done variables should be declared with the keyword volatile. This keyword makes Java aware that the value may be accessed at any time from arbitrary threads.

Another issue is that there’s no parallel execution. Each thread must wait for the other to complete. Of course, this problem does not benefit from a parallelism, but applying this solution to problems which can benefit from parallelism might cause performance problems. Each thread wastes time busy waiting in a while loop for the other to be done, consuming CPU cycles while it does so. You’ll notice that the code must still be carefully written. Each thread must set the other thread’s done value to false. If threads were responsible for setting their own done values to false, one thread might print its information and go back to the top of the for loop before the other thread had reset its own done to false.

In short, coordinating two or more threads together is a difficult problem. None of the solutions we give here are fully acceptable. We introduce better tools for coordination and synchronization in Chapter 14.

13.8. Solution: Deadly virus

Finally, we give the solution to the deadly virus problem. By this point, the threaded part of this problem should not seem very difficult. It’s simpler than some of the examples, such as matrix multiplication. We begin with the worker class FactorThread that can be spawned as a thread.

Program 13.1 Thread class used to find the sum of the two factors of a large odd composite.
public class FactorThread extends Thread {  
    private long lower;
    private long upper; 
    
    public FactorThread(long lower, long upper) {     
        this.lower = lower;
        this.upper = upper;     
    }
    
    public void run() { 
        if(lower % 2 == 0) // Only check odd numbers
            lower++;        
        while(lower < upper) {
            if(Factor.NUMBER % lower == 0) {
                System.out.println("Security code: " + (lower + Factor.NUMBER / lower));
                return;
            }
            lower += 2;
        }           
    }
}

The constructor for FactorThread takes an upper and lower bound, similar to MatrixThread. Once a FactorThread object has those bounds, it can search between them. The number to factor is stored in the Factor class. If any value divides that number evenly, it must be one of the factors, making the other factor easy to find, sum, and print out. We have to add a couple of extra lines of code to make sure that we only search the odd numbers in the range. This solution is tuned for efficiency for this specific security problem. A program to find general prime factors would have to be more flexible. Next, let’s examine the driver program Factor.

Program 13.2 Driver class which creates threads to lower the average search time for the factors of a large odd composite.
public class Factor {
    public static final int THREADS = 4; (1)
    public static final long NUMBER = 59984005171248659L;
    
    public static void main(String[] args) {
        FactorThread[] threads = new FactorThread[THREADS]; (2)
        long root = (long)Math.sqrt(NUMBER); // Go to square root
        long start = 3;  // No need to test 2       
        long quotient = root / THREADS;
        long remainder = root % THREADS;
        
        for(int i = 0; i < THREADS; i++) {
            long work = quotient;
            if(i < remainder)
                work++;
            threads[i] = new FactorThread(start, start + work); (3)
            threads[i].start();
            start += work;
        }   
        try {
            for(int i = 0; i < THREADS; i++)
                threads[i].join(); (4)
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1 Static constants hold both the number to be factored and the number of threads.
2 In the main() method, we create an array of threads for storage.
3 Then, we create and start each FactorThread object, assigning upper and lower bounds at the same time, using the standard technique from Section 6.11 to divide the work fairly. Because we know the number we’re dividing isn’t even, we start with 3. By only going up to the square root of the number, we know that we will only find the smaller of the two factors. In that way we can avoid having one thread find the smaller while another is finds the larger.
4 Afterward, we have the usual join() calls to make sure that all the threads are done. In this problem, these calls are unnecessary. One thread will print out the correct security code, and the others will search fruitlessly. If the program went on to do other work, we might need to let the other threads finish or even interrupt them. Don’t forget join() calls since they’re usually very important.

13.9. Summary

In this chapter we’ve explored two strategies to obtain a concurrent solution to programming problems. One strategy, task decomposition, splits a task into two or more subtasks. These subtasks can then be packaged as Java threads and executed on different cores of a multicore processor. Another strategy, domain decomposition, partitions input data into smaller chunks and allows different threads to work concurrently on each chunk of data.

A concurrent solution to a programming problem can sometimes execute more quickly than a sequential solution. Speedup measure how effective a concurrent solution is at exploiting the architecture of a multicore processor. Note that not all concurrent programs lead to speedup as some run slower than their sequential counterparts. Writing a concurrent program is a challenge that forces us to divide up work and data in a way that best exploits the available processors and OS.

Java provides a rich set of primitives and syntactic elements to write concurrent programs, but only a few of these were introduced in this chapter. Subsequent chapters give additional tools to code more complex concurrent programs.

13.10. Exercises

Conceptual Problems

  1. The start(), run(), and join() methods are essential parts of the process of using threads in Java. Explain the purpose of each method.

  2. What’s the difference between extending the Thread class and implementing the Runnable interface? When should you use one over the other?

  3. How do the Thread.sleep() method and the Thread.yield() method each affect thread scheduling?

  4. Consider the expression in Example 13.2. Suppose that the multiply and exponentiation operations require 1 and 10 time units, respectively. Compute the number of time units required to evaluate the expression as in Figure 13.2(a) and (b).

  5. Suppose that a computer has one quad-core processor. Can the tasks in Example 13.1 and Example 13.2 be further subdivided to improve performance on four cores? Why or why not?

  6. Consider the definition of speedup from Section 13.5. Let’s assume you have a job 1,000,000 units in size. A thread can process 10,000 units of work every second. It takes an additional 100 units of work to create a new thread. What’s the speedup if you have a dual-core processor and create 2 threads? What if you have a quad-core processor and create 4 threads? Or an 8-core processor and create 8 threads? You may assume that a thread does not need to communicate after it’s been created.

  7. In which situations can speedup be smaller than the number of processors? Is it ever possible for speedup to be greater than the number of processors?

  8. Amdahl’s Law is a mathematical description of the maximum amount you can improve a system by only improving a part of it. One form of it states that the maximum speedup attainable in a parallel program is 1/(1 - P) where P is the fraction of the program which can be parallelized to an arbitrary degree. If 30% of the work in a program can be fully parallelized but the rest is completely serial, what’s the speedup with two processors? Four? Eight? What implications does Amdahl’s Law have?

  9. Consider the following table of tasks:

    Task Time Concurrency Dependency

    Washing Dishes

    30

    3

    Cooking Dinner

    45

    3

    Washing Dishes

    Cleaning Bedroom

    10

    2

    Cleaning Bathroom

    30

    2

    Doing Homework

    30

    1

    Cleaning Bedroom

    In this table, the Time column gives the number of minutes a task takes to perform with a single person, the Concurrency column gives the maximum number of people who can be assigned to a task, and the Dependency column shows which tasks can’t start until other tasks have been finished. Assume that people assigned to a given task can perfectly divide the work. In other words, the time a task takes is the single person time divided by the number of people assigned. What’s the minimum amount of time needed to perform all tasks with only a single person? What is the minimum amount of time needed to perform all tasks with an unlimited number of people? What’s the smallest number of people needed to achieve this minimum time?

  10. Consider the following code snippet.

    x = 13;
    x = x * 10;

    Consider this snippet as well.

    x = 7;
    x = x + x;

    If we assume that these two snippets of code are running on separate threads but that x is a shared variable, what are the possible values x could have after both snippets have run? Remember that the execution of these snippets can be interleaved in any way.

Programming Practice

  1. Re-implement the array summing problem from Example 13.10 using polling instead of join() calls. Your program should not use a single call to join(). Polling is not an ideal way to solve this problem, but it’s worth thinking about the technique.

  2. Composers often work with multiple tracks of music. One track might contain solo vocals, another drums, a third one violins, and so on. After recording the entire take, a mix engineer might want to apply special effects such as an echo to one or more tracks.

    To understand how to add echo to a track, suppose that the track consists of a list of audio samples. Each sample in a mono (not stereo) track can be stored as a double in an array. To create an echo effect, we combine the current value of an audio sample with a sample from a fixed time earlier. This time is called the delay parameter. Varying the delay can produce long and short echoes.

    If the samples are stored in array in and the delay parameter is stored in variable delay (measured in number of samples), the following code snippet can be used to create array out which contains the sound with an echo.

    double[] out = new double[in.length + delay];
    // Sound before echo starts
    for(int i = 0; i < delay; i++)
        out[i] = in[i];
    // Sound with echo
    for(int i = delay; i < in.length; i++)
        out[i] = a*in[i] + b*in[i - delay];
    // Echo after sound is over
    for(int i = in.length; i < out.length; i++)
        out[i] = b*in[i - delay];

    Parameters a and b are used to control the nature of the echo. When a is 1 and b is 0, there is no echo. When a is 0 and b is 1, there is no mixing. Audio engineers will control the values of a and b to create the desired echo effect.

    Write a threaded program that computes the values in out in parallel for an arbitrary number of threads.

  3. Write a program which takes a number of minutes and seconds as input. In this program, implement a timer using Thread.sleep() calls. Each second, print the remaining time to the screen. How accurate is your timer?

  4. As you know, π ≈ 3.1416. A more precise value can be found by writing a program which approximates the area of a circle. The area of a circle can be approximated by summing up the area of rectangles filling curve of the arc of the circle. As the width of the rectangle goes to zero, the approximation becomes closer and closer to the true area. If a circle with radius r is centered at the origin, its height y at a particular distance x is given by the following formula.

    circleHeight

    Write a parallel implementation of this problem which divides up portions of the arc of the circle among several threads and then sums the results after they all finish. By setting r = 2, you need only sum one quadrant of a circle to get Ï€. You’ll need to use a very small rectangle width to get an accurate answer. When your program finishes running, you can compare your value against Math.PI for accuracy.

Experiments

  1. Use the currentTimeMillis() method to measure the time taken to execute a relatively long-running piece of Java code you’ve written. Execute your program several times and compare the execution time you obtain during different executions. Why do you think the execution times are different?

  2. Thread creation overhead is an important consideration in writing efficient parallel programs. Write a program which creates a large number of threads which do nothing. Test how long it takes to create and join various numbers of threads. See if you can determine how long a single thread creation operation takes on your system, on average.

  3. Create serial and concurrent implementations of matrix multiplication like those described in Example 13.11.

    1. Experiment with different matrix sizes and thread counts to see how the speedup performance changes. If possible, run your tests on machines with different numbers of cores or processors.

    2. Given a machine with k > 1 cores, what is the maximum speedup you can expect to obtain?

  4. Repeatedly run the code in Example 13.7 which creates several NumberedThread objects. Can you discover any patterns in the order that the threads print? Add a loop and some additional instrumentation to the NumberedThread class which will allow you to measure how long each thread runs before the next thread has a turn.

  5. Create serial and parallel implementations of the array summing problem solved in Example 13.10. Experiment with different array sizes and thread counts to see how performance changes. How does the speedup differ from matrix multiply? What happens if you simply sum the numbers instead of taking the sine first?

  6. The solution to the array summing problem in Example 13.10 seems to use concurrency half-heartedly. After all the threads have computed their sums, the main thread sums up the partial sums sequentially.

    An alternative approach is to sum up the partial sums concurrently. Once a thread has computed the sum of the sines of each partition, the sums of each pair of neighboring partitions should be merged into a single sum. The process can be repeated until the final sum has been computed. At each step, half of the remaining threads will have nothing left to do and will stop. The pattern of summing is like a tree which starts with k threads working at the first stage, k/2 working at the second stage, k/4 working at the third, and so on, until a single thread completes the summing process.

    treesummation
    Figure 13.8 Example of concurrent tree-style summation with 8 threads.

    Update the run() method in the SumThread class so that it adds its assigned elements as before and then adds its neighbor’s sum to its own. To do so, it must use the join() method to wait for the neighboring thread. It should perform this process repeatedly. After summing their own values, each even numbered thread should add in the partial sum from its neighbor. At the next step, each thread with a number divisible by 4 should add the partial sum from its neighbor. At the next step, each thread with a number divisible by 8 should add the partial sum from its neighbor, and so on. Thread 0 will perform the final summation. Consequently, the main thread only needs to wait for thread 0. So that each thread can wait for other threads, the threads array will need to be a static field. Figure 13.8 illustrates this process.

    Once you’ve implemented this design, test it against the original SumThread class to see how it performs. Restrict the number of threads you create to a power of 2 to make it easier to determine which threads wait and which threads terminate.

14. Synchronization

Sharing is sometimes more demanding than giving.

— Mary Catherine Bateson

14.1. Introduction

Concurrent programs allow multiple threads to be scheduled and executed, but the programmer doesn’t have a great deal of control over when threads execute. As explained in Section 13.6, the JVM and the underlying OS are responsible for scheduling threads onto processor cores.

While writing a concurrent program, you have to ensure that the program will work correctly even though different executions of the same program will likely lead to different sequences of thread execution. The problem we introduce next illustrates why one thread execution sequence might be perfectly fine while another might lead to unexpected and incorrect behavior. (And even people starving!)

14.2. Problem: Dining philosophers

Concurrency gives us the potential to make our programs faster but introduces a number of other problems. The way that threads interact can be unpredictable. Because they share memory, one thread can corrupt a value in another thread’s variables. We introduce synchronization tools in this chapter that can prevent threads from corrupting data, but these tools create new pitfalls. To explore these pitfalls, we give you another problem to solve.

Imagine a number of philosophers sitting at a round table with plates that are periodically filled with rice. Between adjacent philosophers are single chopsticks so that there are exactly the same number of chopsticks as there are philosophers. These philosophers only think and eat. In order to eat, a philosopher must pick up both the chopstick on her left and the chopstick on her right. Figure 14.1 illustrates this situation.

diners
Figure 14.1 Table for five dining philosophers.

Your goal is to write a class called DiningPhilosopher which extends Thread. Each thread created in the main() method should be a philosopher who thinks for some random amount of time, then acquires the two necessary chopsticks and eats. No philosopher should starve. No philosophers should be stuck indefinitely fighting over chopsticks.

Although this problem sounds simple, the solution is not. Make sure that you understand the concepts and Java syntax in this chapter thoroughly before trying to implement your solution. It’s important that no two philosophers try to use the same chopstick at the same time. Likewise, we need to avoid a situation where every philosopher is waiting for every other philosopher to give up a chopstick.

14.3. Concepts: Thread interaction

This dining philosopher problem highlights some difficulties which were emerging toward the end of the last chapter. In Exercise 13.10 two snippets of code could run concurrently and modify the same shared variable, potentially producing incorrect output. Because of the nondeterministic nature of scheduling, we have to assume that the code executing in two or more threads can be interleaved in any possible way. When the result of computation changes depending on the order of thread execution, it’s called a race condition. Below is a simple example of a race condition in Java.

Program 14.1 Short example of a race condition.
public class RaceCondition extends Thread {     
    private static int counter = 0; 
    public static final int THREADS = 4;    
    public static final int COUNT = 1000000;        
    
    public static void main(String[] args) {                              
        RaceCondition[] threads = new RaceCondition[THREADS];           
        for(int i = 0; i < THREADS; i++) {
            threads[i] = new RaceCondition();
            threads[i].start();         
        }           
        try {
            for(int i = 0; i < THREADS; i++)
                threads[i].join();
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }           
        System.out.println("Counter:\t" + counter);            
    }   
    
    public void run() { 
        for(int i = 0; i < COUNT/THREADS; i++)
            counter++;
    }
}

This short (and pointless) class attempts to increment the variable counter until it reaches 1000000. To illustrate the race condition, we’ve divided the work of incrementing counter evenly among a number of threads. If you run this program, the final value of counter will often not be 1000000. Depending on which JVM, OS, and how many cores you have, you may never get 1000000, and the answer you do get will vary a lot. On all systems, if you change the value of THREADS to 1, the answer should always be correct.

Looking at the code, the problem might not be obvious. Everything centers on the statement counter++ in the for loop inside the run() method. But, this statement appears to execute in a single step! Each thread should increase the value of counter a total of COUNT/THREADS times, adding up to 1000000. The trouble is that counter++ is not a single step. Recall that counter++ is shorthand for counter = counter + 1. To be even more explicit we could write it as follows.

temp = counter;
counter = temp + 1;

One thread might get as far as storing counter into a temporary location, but then it runs out of time, allowing the next thread in the schedule to run. When that’s the case, this next thread may do a series of increments to count which are all overwritten when the first thread runs again. Because the first thread had an old value of counter stored in temp, adding 1 to temp has the effect of ignoring increments that happened in the interim. This situation can happen on a single processor with threads switching back and forth, but it’s even more dangerous on a multicore system.

The primary lesson here is that threads can switch between each other at any time with unpredictable effects. The secondary lesson is that the source code is too coarse-grained to show atomic operations. An atomic operation is one which cannot be interrupted by a context switch to another thread. The actual code that the JVM runs is much lower level than source code.

We can’t easily force a non-atomic operation to be atomic, but there are ways to restrict access to certain pieces of code under certain conditions. The name we give to a piece of code which should not be accessed by more than one thread at a time is a critical section. In the example above, the single line of code which increments counter is a critical section, and the error in the program would be removed if only one thread were able to run that line of code at a time.

Protecting a critical section is done with mutual exclusion tools. They’re called mutual exclusion tools because they enforce the requirement that one thread executing a critical section excludes the possibility of another. There are many different techniques, algorithms, and language features in computer science which can be used to create mutual exclusion. Java relies heavily on a tool called a monitor which hides some of the details of enforcing mutual exclusion from the user. Mutual exclusion is a deeply researched topic with many approaches other than monitors. If you plan to write concurrent programs in another language, you may need to brush up on its mutual exclusion features.

14.4. Syntax: Thread synchronization

14.4.1. The synchronized keyword

In Java, the language feature which allows you to enforce mutual exclusion is the synchronized keyword. There are two ways to use this keyword: with a method or with an arbitrary block of code. In the method version, you add the synchronized modifier before the return type. Let’s imagine a class with a private String field called message which is set to be "Will Robinson!" by the constructor. Now, we define the following method.

public synchronized void danger() {
    message = "Danger, " + message;
}

If danger() is called five times from different threads, message will contain
"Danger, Danger, Danger, Danger, Danger, Will Robinson!" Without the synchronized keyword, danger() would suffer from a race condition similar to the one in RaceCondition. Some of the String concatenations might be overwritten by other calls to danger(). You would never have more than five copies of "Danger, " appended to the beginning of message, but you might have fewer.

Any time a thread enters a piece of code protected by the synchronized keyword, it implicitly acquires a lock which only a single thread can hold. If another thread tries to access the code, it’s forced to wait until the lock is released. This lock is re-entrant. Re-entrant means that, when a thread currently holds a lock and tries to get it again, it succeeds. This situation frequently occurs with synchronized methods which call other synchronized methods.

Consider method safety() which does the “opposite” of danger(), by removing occurrences of "Danger, " from the beginning of message.

public synchronized void safety() {
    if(message.startsWith("Danger, "))
        message = message.substring(8);
}

Will the danger() and safety() methods play nicely together on the same object? In other words, will a thread be blocked from entering safety() if another thread is already in danger()? Yes! The locks in Java are connected to objects. When you use the synchronized keyword on a method, the object the method is being called on (whichever object this refers to inside the method) serves as the lock. Thus, only one thread can be inside of either of these methods on a given object. If you have 10 synchronized methods in an object, only one of them can execute at a time for that object.

Perhaps this level of control is too restrictive. You may have six methods which conflict with each other and four others which conflict with each other but not the first six. Using synchronized in each method declaration would unnecessarily limit the amount of concurrency your program could have.

Although it takes a little more work, using synchronized with a block of code allows more fine-grained control. The following version of danger() is equivalent to the earlier one.

public void danger() {
    synchronized(this) {
        message = "Danger, " + message;
    }
}

Using synchronized on a block of code gives us more flexibility in two ways. First, we can choose exactly how much code we want to control, instead of the whole method. Second, we can choose which object we want to use for synchronization. For the block style, any arbitrary object can be used as a lock. Objects keep a list of threads which are waiting to get the lock and do all the other management needed to make the synchronized keyword work.

If you have two critical sections which are unrelated to each other, you can use the fine-grained control the block style provides. First, you’ll need some objects to use as locks, probably declared so that they can easily be shared, perhaps as static fields of a class.

private static Object lock1 = new Object();
private static Object lock2 = new Object();

Then, wherever you need control over concurrency, you use them as locks.

synchronized(lock1) {
    // Do dangerous thing 1
}

// Do safe things

synchronized(lock2) {
    // Do dangerous thing 2, unrelated to dangerous thing 1
}

Since declaring a method with synchronized is equivalent to having its body enclosed in a block beginning with synchronized(this), what about static methods? Can they be synchronized? Yes, they can. Whenever a class is loaded, Java creates an object of type Class which corresponds to that class. This object is what synchronized static methods inside the class will use as a lock. For example, a synchronized static method inside of the Eggplant class will lock on the object Eggplant.class.

14.4.2. The wait() and notify() methods

Protecting critical sections with the synchronized keyword is a powerful technique, and many other synchronization tools can be built using just this tool. However, efficiency demands a few more options.

Sometimes a thread is waiting for another thread to finish a task so that it can process the results. Imagine one thread collecting votes while another one’s waiting to count them. In this example, the counting thread must wait for all votes to be cast before it can begin counting. We could use a synchronized block and an indicator boolean called votingComplete to allow the collector thread to signal to the counting thread.

while(true) {
    synchronized(this) {
        if(votingComplete)
            break;
    }
}
countVotes();

What’s the problem with this design? The counting thread is running through the while loop over and over waiting for votingComplete to become true. On a single processor, the counting thread would slow down the job of the collecting thread which is trying to process all the votes. On a multicore system, the counting thread is still wasting CPU cycles that some other thread could use. This phenomenon is known as busy waiting, for obvious reasons.

To combat this problem, Java provides the wait() method. When a thread’s executing synchronized code, it can call wait(). Instead of busy waiting, a thread which has called wait() will be removed from the list of running threads. It will wait in a dormant state until someone comes along and notifies the thread that its waiting is done. If you recall the thread state diagram from Chapter 13, there’s a Not Runnable state which threads enter by calling sleep(), calling wait(), or performing blocking I/O. Using wait(), we can rewrite the vote counting thread.

synchronized(this) {
    while(!votingComplete) {
        wait();
    }
}
countVotes();

Note that the while loop has moved inside the synchronized block. Doing so before might have kept our program from terminating: As long as the vote counting thread held the lock, the vote collecting thread would not be allowed to modify votingComplete. When a thread calls wait(), however, it gives up the corresponding lock it’s holding until it wakes up and runs again. Why use the while loop at all now? There’s no guarantee that the condition you’re waiting for is true. Many threads might be waiting on this particular lock. We use the while loop to check that votingComplete is true and wait again if it isn’t.

In order to notify a waiting thread, the other thread calls the notify() method. Like wait(), notify() must be called within a synchronized block or method. Here is corresponding code the vote collecting thread might use to notify the counting thread that voting is complete.

// Finish collecting votes
synchronized(this) {
    votingComplete = true;
    notifyAll();
}

A call to notify() will wake up one thread waiting on the lock object. If there are many threads waiting, the method notifyAll() used above can be called to wake them all up. In practice, it’s usually safer to call notifyAll(). If a particular condition changes and a single waiting thread is notified, that thread might need to notify the next waiting thread when it’s done. If your code isn’t very carefully designed, some thread might end up waiting forever and never be notified if you only rely on notify().

Example 14.1 Producer/consumer

To illustrate the use of wait() and notify() calls inside of synchronized code, we give a simple solution to the producer/consumer problem below. This problem is a classic example in the concurrent programming world. Often one thread (or a group of threads) is producing data, perhaps from some input operation. At the same time, one thread (or, again, a group of threads) is taking these chunks of data and consuming them by performing some computational or output task.

Every resource inside of a computer is finite. Producer/consumer problems often assume a bounded buffer which stores items from the producer until the consumer can take them away. Our solution does all synchronization on this buffer. Many different threads can share this buffer, but all accesses will be controlled.

Program 14.2 Example of a synchronized buffer.
public class Buffer {
    public final static int SIZE = 10;
    private Object[] objects = new Object[SIZE];    
    private int count = 0;
    
    public synchronized void addItem(Object object) throws InterruptedException { (1)
        while(count == SIZE) (2)
            wait();     
        objects[count] = object;
        count++;
        notifyAll();         (3)
    }
    
    public synchronized Object removeItem() throws InterruptedException { (4)
        while(count == 0)    (5)
            wait();
        count--;
        Object object = objects[count];     
        notifyAll();         (6)
        return object;
    }
}
1 When adding an item, producers enter the synchronized addItem() method.
2 If count shows that the buffer is full, the producer must wait until the buffer has at least one open space.
3 After adding an item to the buffer, the producer then notifies all waiting threads.
4 The consumer performs mirrored operations in removeItem().
5 A consumer thread can’t consume anything if the buffer is empty and must then wait.
6 After there’s an object to consume, the consumer removes it and notifies all other threads.

Both methods are synchronized, making access to the buffer completely sequential. Although it seems undesirable, sequential behavior is precisely what’s needed for the producer/consumer problem. All synchronized code is a protection against unsafe concurrency. The goal is to minimize the amount of time spent in synchronized code and get threads back to parallel execution as quickly as possible.

Example 14.2 Bank account

Although producer/consumer is a good model to keep in mind, there are other ways that reading and writing threads might interact. Consider the following programming problem, similar to one you might find in real life.

As a rising star in a bank’s IT department, you’ve been given the job of creating a new bank account class called SynchronizedAccount. This class must have methods to support the following operations: deposit, withdraw, and check balance. Each method should print a status message to the screen on completion. Also, the method for withdraw should return false and do nothing if there are insufficient funds. Because the latest system is multi-threaded, these methods must be designed so that the bookkeeping is consistent even if many threads are accessing a single account. No money should magically appear or disappear.

There’s an additional challenge. To maximize concurrency, SynchronizedAccount should be synchronized differently for read and write accesses. Any number of threads should be able to check the balance on an account simultaneously, but only one thread can deposit or withdraw at a time.

To solve this problem, our implementation of the class has a balance variable to record the balance, but it also has a readers variable to keep track of the number of threads which are reading from the account at any given time.

public class SynchronizedAccount {
    private double balance = 0.0;   
    private int readers = 0;    

Next, the getBalance() method is called by threads which wish to read the balance.

    public double getBalance() throws InterruptedException {
        double amount;      
        synchronized(this) {   (1)
            readers++;
        }       
        amount = balance;      (2)
        synchronized(this) {
            if(--readers == 0) (3)
                notifyAll();   (4)
        }       
        return amount;      
    }
1 Access to the readers variable is synchronized.
2 After passing that first synchronized block, the code which stores the balance is no longer synchronized. In this way, multiple readers can access the data at the same time. For this example, the concurrency controls we have are overkill. The command amount = balance does not take a great deal of time. If it did, however, it would make sense for readers to execute it concurrently as we do.
3 After reading the balance, this method decrements readers.
4 If readers reaches 0, a call to notifyAll() is made, signaling that threads trying to deposit to or withdraw from the account can continue.
    public void deposit(double amount) throws InterruptedException {
        changeBalance(amount);
        System.out.println("Deposited $" + amount + ".");
    }
    
    public boolean withdraw(double amount)
        throws InterruptedException {
        boolean success = changeBalance(-amount);
        if(success)
            System.out.println("Withdrew $" + amount + ".");
        else
            System.out.println("Failed to withdraw $" +
                amount + ": insufficient funds.");
        return success;
    }

The deposit() and withdraw() methods are wrappers for the changeBalance() method, which has all the interesting concurrency controls.

    protected synchronized boolean changeBalance(double amount) (1)
        throws InterruptedException {
        boolean success;    
        while(readers > 0) (2)
			wait();         
        if(success = (balance + amount > 0)) (3)
            balance += amount;      
        return success; 
    }
}
1 The changeBalance() method is synchronized so that it can have exclusive access to the readers variable. It’s also marked protected because SynchronizedAccount will be used as a parent class in Chapter 17.
2 As long as readers is greater than 0, this method will wait.
3 Eventually, the readers should finish their job and notify the waiting writer which can finish changing the balance of the account.

14.5. Pitfalls: Synchronization challenges

As you can see from the dining philosophers problem, synchronization tools help us get the right answer but also create other difficulties.

14.5.1. Deadlock

Deadlock is the situation when two or more threads are both waiting for the others to complete, forever. Some combination of locks or other synchronization tools has forced a blocking dependence onto a group of threads which will never be resolved.

In the past, people have described four conditions which must exist for deadlock to happen.

  1. Mutual Exclusion: Only one thread can access the resource (often a lock) at a time.

  2. Hold and Wait: A thread holding a resource can ask for additional resources.

  3. No Preemption: A thread holding a resource cannot be forced to release it by another thread.

  4. Circular Wait: Two or more threads hold resources which make up a circular chain of dependency.

Example 14.3 Deadlock philosophers

We illustrate deadlock with an example of how not to solve the dining philosophers problem. What if all the philosophers decided to pick up the chopstick on her left and then the chopstick on her right? If the timing was just right, each philosopher would be holding one chopstick in her left hand and be waiting forever for her neighbor on the right to give up a chopstick. No philosopher would ever be able to eat. Here’s that scenario illustrated in code.

public class DeadlockPhilosopher extends Thread {
    public static final int SEATS = 5;     (1)
    private static boolean[] chopsticks = new boolean[SEATS]; (2)
    private int seat;
    
    public DeadlockPhilosopher(int seat) { (3)
        this.seat = seat;
    }
1 We define a constant for the number of seats.
2 We create a shared boolean array called chopsticks so that all philosophers can know which chopsticks are in use.
3 The constructor assigns each philosopher a seat number.
	public static void main(String args[]) {        
        DeadlockPhilosopher[] philosophers = new DeadlockPhilosopher[SEATS];
        for(int i = 0; i < SEATS; i++) {
            philosophers[i] = new DeadlockPhilosopher(i);
            philosophers[i].start();    (1)
        }
        try {
            for(int i = 0; i < SEATS; i++)                        
                philosophers[i].join(); (2)
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }       
        System.out.println("All philosophers done.");
    }
1 In main(), we create and start a thread for each philosopher.
2 Then, we wait for them to finish which, sadly, will never happen.

After setting up the class and the main() method, things get interesting in the run() method.

    public void run() {         
        try { 
            getChopstick(seat);     			(1)
            Thread.sleep(50);       			(2)
			getChopstick((seat + 1) % SEATS); 	(3)
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }           
        eat();
    }
1 First a philosopher tries to get her left chopstick.
2 Then she sleeps for 50 milliseconds.
3 Finally, she tries to get her right chopstick. We mod by SEATS so that the last philosopher will try to get the chopstick at the beginning of the array.

Without sleeping, this code would usually run just fine. Every once in a while, the philosophers would become deadlocked, but it would be hard to predict when. By introducing the sleep, we can all but guarantee that the philosophers will deadlock every time.

The remaining two methods are worth examining to see how the synchronization is done, but by getting the two chopsticks separately above, we’ve already gotten ourselves into trouble.

    private void getChopstick(int location) throws InterruptedException {
        if(location < 0)
            location += SEATS;
        synchronized(chopsticks) {
            while(chopsticks[location])
                chopsticks.wait();
            chopsticks[location] = true;
        }       
        System.out.println("Philosopher " + seat +
            " picked up chopstick " + location + ".");
    }
    
    private void eat() {
        // Done eating, put back chopsticks
        synchronized(chopsticks) {
            chopsticks[seat] = false;           
            if(seat == 0)
                chopsticks[SEATS - 1] = false;
            else
                chopsticks[seat - 1] = false;                           
            chopsticks.notifyAll();
        }
    }
}    
Example 14.4 Deadlock sum

Here’s another example of deadlock. We emphasize deadlock because it’s one of the most common and problematic issues with using synchronization carelessly.

Consider two threads which both need access to two separate resources. In our example, the two resources are random number generators. The goal of each of these threads is to acquire locks for the two shared random number generators, generate two random numbers each, and sum the numbers generated. (Note that locks are totally unnecessary for this problem since access to Random objects is synchronized.)

import java.util.Random;

public class DeadlockSum extends Thread {
    private static Random random1 = new Random();
    private static Random random2 = new Random();   
    private boolean reverse;
    private int sum;

The class begins by creating shared static Random objects random1 and random2. Then, in the main() method, the main thread spawns two new threads, passing true to one and false to the other.

    public static void main(String[] args) {
        Thread thread1 = new DeadlockSum(true);
        Thread thread2 = new DeadlockSum(false);
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }                   
    }

Next, the mischief begins to unfold. One of the two threads stores true in its reverse field.

    public DeadlockSum(boolean reverse) {
        this.reverse = reverse;
    }

Finally, we have the run() method where all the action happens. If the two running threads were both to acquire locks for random1 and random2 in the same order, everything would work out fine. However, the reversed thread locks on random2 and then random1, with a sleep() in between. The non-reversed thread tries to lock on random1 and then random2.

    public void run() { 
        if(reverse) {         
            synchronized(random2) {
				System.out.println("Reversed Thread: locked random2");
				try{ Thread.sleep(50); }
				catch(InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(random1) {
					System.out.println("Reversed Thread: locked random1");
					sum = random1.nextInt() + random2.nextInt();                
				}
            }
        }
        else {          
            synchronized(random1) {
				System.out.println("Normal Thread: locked random1");
				try { Thread.sleep(50); }
				catch(InterruptedException e) {
				  e.printStackTrace();
				}
				synchronized(random2) {
					System.out.println("Normal Thread: locked random2");              
					sum = random1.nextInt() + random2.nextInt();                
				}
			}
        }
    }
}

If you run this code, it should invariably deadlock with thread1 locked on random2 and thread2 locked on random1. No sane programmer would intentionally code the threads like this. In fact, the extra work we did to acquire the locks in opposite orders is exactly what causes the deadlock. For more complicated programs, there may be many different kinds of threads and many different resources. If two different threads (perhaps written by different programmers) need both resource A and resource B at the same time but try to acquire them in reverse order, this kind of deadlock can occur without such an obvious cause.

For deadlock of this type, the circular wait condition can be broken by ordering the resources and always locking the resources in ascending order. Of course, this solution only works if there is some universal way of ordering the resources and the ordering is always followed by all threads in the program.

Ignoring the deadlock problems with the example above, it gives a nice example of the way Java intended synchronization to be done: when possible, use the resource you need as its own lock. Many other languages require programmers to create additional locks or semaphores to protect a given resource, but this approach causes problems if the same lock is not consistently used. Using the resource itself as a lock is an elegant solution.

14.5.2. Starvation and livelock

Starvation is another problem which can occur with careless use of synchronization tools. Starvation is a general term which covers any situation in which some thread never gets access to the resources it needs. Deadlock can be viewed as a special case of starvation since none of the threads which are deadlocking makes progress.

The dining philosophers problem was framed around the idea of eating with humorous intent. If a philosopher is never able to acquire chopsticks, that philosopher will quite literally starve.

Starvation doesn’t necessarily mean deadlock, however. Examine the implementation in Example 14.2 for the bank account. That solution is correct in the sense that it preserves mutual exclusion. No combination of balance checks, deposits, or withdrawals will cause the balance to be incorrect. Money will neither be created nor destroyed. A closer inspection reveals that the solution is not entirely fair. If a single thread is checking the balance, no other thread can make a deposit or a withdrawal. Balance checking threads could be coming and going constantly, incrementing and decrementing the readers variable, but if readers never goes down to zero, threads waiting to make deposits and withdrawals will wait forever.

Another kind of starvation is livelock. In deadlock, two or more threads get stuck and wait forever, doing nothing. Livelock is similar except that the two threads keep executing code and waiting for some condition that never arrives. A classic example of livelock is two polite (but oddly predictable) people speaking with each other: Both happen to start talking at exactly the same moment and then stop to hear what the other has to say. After exactly one second, they both begin again and immediately stop. Lather, rinse, repeat.

Example 14.5 Livelock party preparations

Imagine three friends going to a party. Each of them starts getting ready at different times. They follow the pattern of getting ready for a while, waiting for their friends to get ready, and then calling their friends to see if the other two are ready. If all three are ready, then the friends will leave. Unfortunately, if a friend calls and either of the other two aren’t ready, he’ll become frustrated and stop being ready. Perhaps he’ll realize that he’s got time to take a shower or get involved in some other activity for a while. After finishing that activity, he’ll become ready again and wait for his friends to become ready.

If the timing is just right, the three friends will keep becoming ready, waiting for a while, and then becoming frustrated when they realize that their friends aren’t ready. Here’s a rough simulation of this process in code.

public class Livelock extends Thread {
    private static int totalReady = 0; 		   (1)
    private static Object lock = new Object(); (2)

    public static void main(String[] args) {   (3)
        Livelock friend1 = new Livelock();
        Livelock friend2 = new Livelock();
        Livelock friend3 = new Livelock();
1 First, we create a shared variable called totalReady which tracks the total number of friends ready.
2 To avoid race conditions, a shared Object called lock will be used to control access to totalReady.
3 Then, the main() method creates Livelock objects representing each of the friends.
        try {       
            friend1.start();
            Thread.sleep(100);
            friend2.start();
            Thread.sleep(100);
            friend3.start();
                        
            friend1.join();
            friend2.join();
            friend3.join();
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }       
        System.out.println("All ready!");
    }

The rest of the main() method starts each of the threads representing the friends running, with a 100 millisecond delay before the next thread starts . Then, it waits for them all to finish. If successful, it’ll print All ready! to the screen.

    public void run() { 
        boolean done = false;
    
        try {       
            while(!done) { (1)
                Thread.sleep(75); // Prepare for party (2)
                synchronized(lock) {
                    totalReady++;       (3)
                }                   
                Thread.sleep(75); // Wait for friends  (4)
                synchronized(lock) {
                    if(totalReady >= 3) (5)
                        done = true;
                    else
                        totalReady--;   (6)
                }
            }
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1 In the run() method, each friend goes through a loop until the done variable is true.
2 In this loop, an initial call to Thread.sleep() for 75 milliseconds represents preparing for the party.
3 After that, totalReady is incremented by one.
4 Then, the friend waits for another 75 milliseconds.
5 Finally, he checks to see if everyone else is ready by testing whether totalReady is 3.
6 If not, he decrements totalReady and repeats the process.

At roughly 75 milliseconds into the simulation, the first friend becomes ready, but he doesn’t check with his friends until 150 milliseconds. Unfortunately, the second friend doesn’t become ready until 175 milliseconds. He then checks with his friends at 225 milliseconds, around which time the first friend is becoming ready a second time. However, the third friend isn’t ready until 275 milliseconds. When he then checks at 350 milliseconds, the first friend isn’t ready anymore. On some systems the timing might drift such that the friends all become ready at the same time, but it could take a long, long while.

In reality, human beings would not put off going to a party indefinitely. Some people would decide that it was too late to go. Others would go alone. Others would go over to their friends' houses and demand to know what was taking so long. Computers are not nearly as sensible and must obey instructions, even if they cause useless repetitive patterns. Realistic examples of livelock are hard to show in a short amount of code, but they do crop up in real systems and can be very difficult to predict.

14.5.3. Sequential execution

When designing a parallel program, you might notice that synchronization tools are necessary to get a correct answer. Then, when you run this parallel version and compare it to the sequential version, it runs no faster or, worse, runs slower than the sequential version. Too much zeal with synchronization tools can produce a program which gives the right answer but doesn’t exploit any parallelism.

For example, we can take the run() method from the parallel implementation of matrix multiply given in Example 13.11 and use the synchronized keyword to lock on the matrix itself.

public void run() {
    synchronized(c) {
        for(int i = lower; i < upper; i++)
            for(int j = 0; j < c[i].length; j++)
                for(int k = 0; k < b.length; k++)
                    c[i][j] += a[i][k] * b[k][j];
    }
}

In this case, only a single thread would have access to the matrix at any given time, and all speedup would be lost.

For the parallel version of matrix multiply we gave earlier, no synchronization is needed. In the case of the producer/consumer problem, synchronization is necessary, and the only way to manage the buffer properly is to enforce sequential execution. Sometimes sequential execution can’t be avoided, but you should always know which pieces of code are truly executing in parallel and which aren’t if you hope to get the maximum amount of speedup. The synchronized keyword should be used whenever it’s needed, but no more.

14.5.4. Priority inversion

In Chapter 13 we suggest that you rarely use thread priorities. Even good reasons to use priorities can be thwarted by priority inversion. In priority inversion, a lower priority thread holds a lock needed by a higher priority thread, potentially for a long time. Because the high priority thread cannot continue, the lower priority thread gets more CPU time, as if it were a high priority thread.

Worse, if there are some medium priority threads in the system, the low priority thread could hold the lock needed by the high priority thread for even longer because those medium priority threads reduce the amount of CPU time the low priority thread has to finish its task.

14.6. Solution: Dining philosophers

Here we give our solution to the dining philosophers problem. Although deadlock was the key pitfall we were trying to avoid, many other issues can crop up in solutions to this problem. A single philosopher might be forced into starvation, or all philosophers might experience livelock through some pattern of picking up and putting down chopsticks which never quite works out. A very simple solution could allow the philosophers to eat, one by one, in order. Then, the philosophers would often and unnecessarily be waiting to eat, and the program would approach sequential execution.

The key element that makes our solution work is that we force a philosopher to pick up two chopsticks atomically. The philosopher will either pick up both chopsticks or neither.

import java.util.Random;

public class DiningPhilosopher extends Thread {
    public static final int SEATS = 5; 
    private static boolean[] chopsticks = new boolean[SEATS];   
    private int seat;
    
    public DiningPhilosopher(int seat) {
        this.seat = seat;       
    }

We begin with a similar setup as the deadlocking version given in Example 14.3.

    public static void main(String args[]) {        
        DiningPhilosopher[] philosophers = new DiningPhilosopher[SEATS];
        for(int i = 0; i < SEATS; i++) {
            philosophers[i] = new DiningPhilosopher(i);
            philosophers[i].start();    (1)
        }
        try {
            for(int i = 0; i < SEATS; i++)                        
                philosophers[i].join(); (2)
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }       
        System.out.println("All philosophers done.");
    }
1 In main(), we create and start a thread for each philosopher.
2 Then, we wait for them to finish.
    public void run() {               	(1)
        for(int i = 0; i < 100; i++) {	(2)
            think();				   
            getChopsticks();           
            eat();
        }
    }
    
    private void think() {			  	(3)
        Random random = new Random();
        try {
            sleep(random.nextInt(20) + 10);
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
1 This run() method is different from the deadlocking version but not in a way that prevents deadlock.
2 We added the for loop so that you could see the philosophers eat and think many different times without problems.
3 We also added the think() method to randomize the amount of time between eating so that each run of the program is less deterministic.
    private void getChopsticks() {	  	(1)
        int location1 = seat;
        int location2 = (seat + 1) % SEATS;              
        synchronized(chopsticks) {	  	(2)
            while(chopsticks[location1] || chopsticks[location2]) { (3)
                try {
                    chopsticks.wait();	(4)
                }
                catch(InterruptedException e) {
                    e.printStackTrace();
                }                               
            }           
            chopsticks[location1] = true;
            chopsticks[location2] = true;
        }       
        System.out.println("Philosopher " + seat + " picked up chopsticks " +
			location1 + " and " + location2 + ".");
    }
1 The real place where deadlock is prevented is in the getChopsticks() method. As in Example 14.3, we mod by SEATS so that the last philosopher tries to get the first chopstick in the array.
2 The philosopher acquires the chopsticks lock.
3 Then, she picks up the two chopsticks she needs only if both are available.
4 Otherwise, she waits.
    private void eat() { 			  	(1)
        // Done eating, put back chopsticks
        synchronized(chopsticks) {	  	(2)
            chopsticks[seat] = false;           
            if(seat == 0)
                chopsticks[SEATS - 1] = false;
            else
                chopsticks[seat - 1] = false;               
            chopsticks.notifyAll();   	(3)
        }
    }
}
1 Finally, in the eat() method, the philosopher eats the rice. We would assume that some other computation would be done here in a realistic problem before entering the synchronized block. The eating itself does not require a lock.
2 After eating’s done, the lock is acquired to give back the chopsticks (hopefully after some cleaning).
3 Then, all waiting philosophers are notified that some chopsticks may have become available.

Our solution prevents deadlock and livelock because some philosopher will get control of two chopsticks eventually, yet there are still issues. Note that each philosopher only eats and thinks 100 times. If, instead of philosophers sharing chopsticks, each thread were a server sharing network storage units, the program could run for an unspecified amount of time: days, weeks, even years. If starvation is happening to a particular philosopher in our program, the other philosophers will finish after 100 rounds, and the starved philosopher can catch up. If there were no limitation on the loop, a starving philosopher might never catch up.

Even if we increase the number of iterations of the loop quite a lot, we probably wouldn’t see starvation of an individual thread because we’re cheating in another way. Some unlucky sequence of chopstick accesses by two neighboring philosophers could starve the philosopher between them. By making the think() method wait a random amount of time, such a sequence will probably be interrupted. If all philosophers thought for exactly the same amount of time each turn, an unlucky pattern could repeat. It’s not unreasonable to believe that the amount of thinking a philosopher (or a server) will do at any given time will vary, but the behavior depends on the system.

It’s very difficult to come up with a perfect answer to some synchronization problems. Such problems have been studied for many years, and research continues to find better solutions.

14.7. Exercises

Conceptual Problems

  1. What’s the purpose of the synchronized keyword? How does it work?

  2. The language specification for Java makes it illegal to use the synchronized keyword on constructors. During the creation of an object, it’s possible to leak data to the outside world by adding a reference to the object under construction to some shared data structure. What’s the danger of leaking data in this way?

  3. If you call wait() or notify() on an object, it must be inside of a block synchronized on the same object. If not, the code will compile, but an IllegalMonitorStateException may be thrown at run time. Why is it necessary to own the lock on an object before calling wait() or notify() on it?

  4. Why is it safer to call notifyAll() than notify()? If it’s generally safer to call notifyAll(), are there any scenarios in which there are good reasons to call notify()?

  5. Imagine a simulation of a restaurant with many waiter and chef objects. The waiters must submit orders to the kitchen staff, and the chefs must divide the work among themselves. How would you design this system? How would information and food be passed from waiter to chef and chef to waiter? How would you synchronize the process?

  6. What’s a race condition? Give a real life example of one.

  7. Let’s reexamine the code that increments a variable with several threads from Section 14.3. We can rewrite the run() method as follows.

    public synchronized void run() {
        for(int i = 0; i < COUNT/THREADS; i++)
            counter++;
    }

    Will this change fix the race condition? Why or why not?

  8. Examine our deadlock example from Example 14.4. Explain why this example fulfills all four conditions for deadlock. Be specific about which threads and which resources are needed to show each condition.

  9. What’s priority inversion? Why can a low priority thread holding a lock be particularly problematic?

Programming Practice

  1. In Example 14.1 the Buffer class used to implement a solution to the producer/consumer problem only has a single lock. When the buffer is empty and a producer puts an item in it, both producers and consumers are woken up. A similar situation happens whenever the buffer is full and a consumer removes an item. Re-implement this solution with two locks so that a producer putting an item into an empty buffer only wakes up consumers and a consumer removing an item from a full buffer only wakes up producers.

  2. In Example 14.2 we used the class SynchronizedAccount to solve a bank account problem. As we mention in Section 14.5.2, depositing and withdrawing threads can be starved out by a steady supply of balance checking threads. Add additional synchronization tools to SynchronizedAccount so that balance checking threads will take turns with depositing and withdrawing threads. If there are no depositing or withdrawing threads, make your implementation continue to allow an unlimited number of balance checking threads to read concurrently.

  3. The solution to the dining philosophers problem given in Section 14.6 suffers from the problem that a philosopher could be starved by the two philosophers on either side of her, if she happened to get unlucky. Add variables to each philosopher which indicate hunger and the last time a philosopher has eaten. If a given philosopher is hungry and hasn’t eaten for longer than her neighbor, her neighbor shouldn’t pick up the chopstick they share. Add synchronization tools to enforce this principle of fairness. Note that your solution must not cause deadlock. Although one philosopher may be waiting on another who is waiting on another and so on, some philosopher in the circle must have gone hungry the longest, breaking circular wait.

Experiments

  1. Critical sections can slow down your program by preventing parallel computation. However, the locks used to enforce critical sections can add extra delays on top of that. Design a simple experiment which repeatedly acquires a lock and does some simple operation. Test the running time with and without the lock. See if you can estimate the time needed to acquire a lock in Java on your system.

  2. Design a program which experimentally determines how much time a thread is scheduled to spend running on a CPU before switching to the next thread. To do this, first create a tight loop which runs a large number of iterations, perhaps 1,000,000 or more. Determine how much time it takes to run a single run of those iterations. Then, write an outer loop which runs the tight loop several times. Each iteration of the outer loop, test to see how much time has passed. When you encounter a large jump in time, typically at least 10 times the amount of time the tight loop usually takes to run to completion, record that time. If you run these loops in multiple threads and average the unusually long times together for each thread, you should be able to find out about how long each thread waits between runs. Using this information, you can estimate how much time each thread is allotted. Bear in mind that your this average is only an estimation. Some JVMs will change the amount of CPU time allotted to threads for various reasons. If you’re on a multicore machine, it will be more difficult to interpret your data since some threads will be running concurrently.

  3. Create an experiment to investigate priority inversion in the following way.

    1. Create two threads, setting the priority of the first to MIN_PRIORITY and the priority of the second to MAX_PRIORITY. Start the first thread running but wait 100 milliseconds before starting the second thread. The first thread should acquire a shared lock and then perform some lengthy process such as finding the sum of the sines of the first million integers. After it finishes its computation, it should release the lock, and print a message. The second thread should try to acquire the lock, print a message, and then release the lock. Time the process. Because the lock is held by the lower priority thread, the higher priority thread will have to wait until the other thread is done for it to finish.

    2. Once you have a feel for the time it takes for these two threads to finish alone, create 10 more threads that must also perform a lot of computation. However, do not make these threads try to acquire the lock. How much do they delay completion of the task? How does this delay relate to the number of cores in your processor? How much does the delay change if you set the priorities of these new threads to MAX_PRIORITY or MIN_PRIORITY?

15. Constructing Graphical User Interfaces

A good sketch is better than a long speech.

— Napoleon Bonaparte

15.1. Problem: Math tutor

Most people are used to using interacting with programs through a GUI rather than a command-line interface. In this chapter, our problem is to write a GUI program that can allow young math students to practice their arithmetic. Specifically, we’re interested in addition, subtraction, multiplication, and division with small, positive integers. For this program, we’ll consider addition and subtraction basic and multiplication and division advanced. Our program should allow the user to select a check box in a menu setting the mode to advanced or basic.

The user should then be able to select one of the four operations from another menu. Once the operation is selected, the program should generate a random problem testing that operation. The problem should be displayed as a label on the program with a text field to one side. The user should be able to enter an answer in the text field and hit a button to submit it. The program should check the answer and display the updated number of correct and incorrect answers.

mathTutorFigure
Figure 15.1 GUI for the MathTutor. (a) No menu selected. (b) Type selected. (c) Operations selected. (d) Attempting to answer an addition problem.

Figure 15.1 shows the final program in four different states. The window on the top left appears when the program is first initialized, with the “Submit” button disabled until a problem is generated. The top right and the bottom left show each of the two menus open. The bottom right shows the program when an addition problem has been generated and the user is about to answer.

15.2. Concepts: Graphical user interfaces

The program shown above with its menus, labels, buttons, and other interactive components is called a graphical user interface or GUI. A GUI is a means of communication between a computer program and a (usually human) user. Although it’s possible for some programs such as scripts to interact with a GUI, most program-to-program communication is done in other ways.

While communication through input and output on the command line is one way to communicate with a program, GUIs offer a user-friendly alternative that has become extremely commonplace. In fact, GUIs are so common that many people have never used anything else to interact with programs and may not even suspect that other kinds of interaction are possible.

This chapter will teach you how to write programs with GUIs. Although GUIs can make input and output easier for the user, the programmer has to shoulder the burden of arranging the layout and appearance of the GUI and making it function properly. Chapter 7 introduced a way to make simple GUIs, but those GUIs came in preset flavors designed for displaying a message, getting a range of responses in the form of buttons or lists, or reading a short piece of text as input. In this chapter we’ll explore ways to make GUIs of arbitrary complexity with no limitations on the size or shape of the GUI or the components it contains.

A typical GUI consists of a frame (also known as a window) on which are displayed one or more components (known as widgets), such as panels, buttons, and text boxes. Panels are used to organize the contents within the frame. A frame contains at least one panel, but additional ones can be added. Each panel can also contain components: buttons, labels, text boxes, and even other panels. Using code to create a GUI with all the components laid out exactly where you want them is half the work of making a GUI-driven program in Java.

Some components like labels are read-only to the user. They display information such as status messages. Other components allow the user to give input to the program. Reading GUI input is usually done in response to an event. Handling events inside of a program is the other half of writing a GUI program in Java. Layout is concerned with appearance, but event handling is concerned with functionality.

Some IDEs such as IntelliJ have graphical tools that automatically generate GUI layout code for you. It’s fast to create a GUI with such tools, but the GUIs they create are often inflexible and look terrible when the window is resized. We focus on how to write all the code yourself because doing so gives you more control and helps you understand Java GUIs better.

15.2.1. Swing and AWT

Most of the components you’ll use to create GUIs are defined in classes that belong to the Java Swing library. This library contains many interfaces and classes. In earlier chapters, you have seen and used one class from the Swing library, the JOptionPane class.

Some components of the Swing library are built on another library known as the Abstract Window Toolkit or AWT. The AWT is an older library which provides direct access to OS components. Thus, an AWT Button object in Microsoft Windows creates a Windows button. Swing, however, draws its own button. AWT GUIs look exactly like other GUIs from the same OS. Swing GUIs can be configured to look similar using look and feel settings, or developers can choose to use a default Java look and feel that will look similar across all platforms.

We’ll discuss many Swing components such as JButton and JTextField. Swing components usually have a J at the beginning of their names to distinguish them from similar AWT components. (The AWT contains Button and TextField. If you see examples from other sources using components that don’t start with J, they’re probably using AWT.) Although Swing is built on top of the older AWT library, it’s not a good idea to mix Swing and AWT components in a single GUI.

The Swing library is far too large for us to cover in its entirety. Instead, our goal is to show you how to construct GUIs using some of the most common Java components. Once you’ve grasped the material in this chapter, you’ll be able to read and understand how to use many other interesting Swing components, as well as other Java libraries for constructing GUIs. The JavaFX library is a newer library that was intended to replace Swing, but it hasn’t been widely adopted. Although it’s still available, JavaFX is no longer included with Java 11 and higher and must be downloaded as a separate library.

15.3. Syntax: GUIs in Java

15.3.1. Creating a frame

A frame is the Java terminology for a window. GUI components in Java will usually be found on a frame. In Swing, a frame is an object whose type is derived from the JFrame class. Here’s a line of code that creates a JFrame object.

JFrame frame = new JFrame("Empty Frame");

The above statement declares and creates a JFrame object named frame. The title of the frame is “Empty Frame” and is given as an argument to the JFrame constructor. Although calling the constructor creates the frame, you need to make it visible for it to show up on the screen.

frame.setVisible(true);

The setVisible() method causes the frame frame to be visible on the screen as a window. You can specify its size as follows.

frame.setSize(350,200);

The setSize() method sets the width and height of the frame in pixels. In the above example, the width of frame is set to 350 pixels and its height to 200 pixels. If you don’t set the size (or use a method like pack() to make a JFrame resize itself to the appropriate size for its contents), it might be so tiny that you at first don’t see it.

The window created by the above code is resizable. If you don’t want the user to be able to resize the window, you can specify that as well.

frame.setResizable(false);

Your entire GUI will often be a frame you create and the components inside that frame. You might want your application to end when you close the frame by clicking on the close button toward the top. The actual location of this button depends on the operating system and the look and feel managers you’re using. Regardless, the following statement can be used to set the behavior of the application when you close the frame window.

frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
Pitfall: Closing frames

Some books and online tutorials suggest setting JFrame.EXIT_ON_CLOSE as the default close operation for a JFrame instead of JFrame.DISPOSE_ON_CLOSE. In our opinion, this option should never be used.

Using JFrame.EXIT_ON_CLOSE is equivalent to calling System.exit(0), which shuts down the JVM. In a single-threaded application, the difference between disposing and exiting is small. However, in a multi-threaded application, shutting down the JVM means killing off all the other threads, no matter what task they’re performing. In general, System.exit() shouldn’t be called, whether the program has a GUI or not.

By using JFrame.DISPOSE_ON_CLOSE, the frame releases all the resources it’s been using and terminates any threads it uses to redraw itself and check for events. If there are no other threads running, the application will then shut down.

It’s still necessary to select some default closing operation. By default, the operation is set to JFrame.HIDE_ON_CLOSE, which hides the frame but doesn’t end its threads. Unfortunately, if the frame is the only way you have of interacting with your application, you can no longer use it when it’s hidden! At that point, you may have to use a process or task manager to shut down the JVM manually.

Example 15.1 Empty frame

Program 15.1 creates and displays an empty frame with the title “Empty Frame,” much like the code above.

Program 15.1 Creates an empty frame.
import javax.swing.*; (1)

public class EmptyFrame {
    public static void main(String[] args){
        JFrame frame = new JFrame("Empty Frame"); (2)
        frame.setSize(350,200); (3)
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); (4)
        frame.setVisible(true); (5)
    }  
}
1 This import statement lets the compiler know we’re using the Swing library.
2 The first statement inside the main() method declares and creates a JFrame object and assigns a title to it.
3 Then, we set its size.
4 Next, we set the default close operation.
5 Finally, we make the frame visible.

The frame so created is shown in Figure 15.2.

emptyFrameFigure
Figure 15.2 An empty frame titled “Empty Frame.”

You may resize the frame at any point in the program even after the frame has been created and made visible. The initial size may or may not be set prior to making the frame visible. Similarly, the frame title can be set and reset at any point in the program.

A single frame might need to open other subsidiary windows from time to time to ask a question or display some information. To create windows dependent on a parent frame, use JDialog instead of JFrame. A dialog window made with JDialog can either be modal (meaning that the user must close it before they can interact with the parent frame) or non-modal (allowing the user to interact with it and its parent frame at the same time).

15.3.2. Components

A component (or widget) is an element of a GUI. Java provides a large variety of components including panels, buttons, text boxes, check boxes, radio buttons, and menus. When laying out a GUI, one or more of the components are created and then placed on a frame. A component is declared and created like any other object.

Widget w = new Widget(arguments);

Here we use class Widget to represent any Swing component class like JButton or JTextField. Arguments supplied while constructing a component allow you to set attributes such as its icon, color, size, or text to display. Most of these attributes can be changed after creation.

Although components can be added directly to a frame, it’s often convenient to lay out a GUI by adding panels to a frame and adding components to those panels. Each panel can hold zero or more components. A panel is also referred to as a container object. Next, we show you how to create a panel, populate it with components, add it to a frame, and display the completed frame.

In Swing, a panel is an instance of the JPanel class and can be created as follows.

JPanel panel = new JPanel();

This statement creates a panel named panel. Thus far, the panel is empty. We can create two buttons and add them to panel.

JButton thisButton = new JButton("This");
JButton thatButton = new JButton("That");
panel.add(thisButton);
panel.add(thatButton);

Here, we create two buttons named thisButton and thatButton. It’s common for component variables to reflect what kind of component they are by appending a description, in this case Button, to their names. These buttons are labeled "This" and "That", but their labels could be any String values. Then, we add the two buttons to the panel.

frame.add(panel);

This statement adds the panel to an existing frame called frame.

Another useful component is JTextField. It creates a text field that can be used by a program for both input and output of String data.

JTextField field = new JTextField("This is not a pipe.");

This statement creates a JTextField component named field. When displayed, it’ll show the text “This is not a pipe.” The user can change this text by typing, but we won’t know when the text has been change without the event handling tools discussed in Section 15.3.3. The following example combines several of the components we’ve introduced into one program.

Example 15.2 GUI with buttons and text field

We can write an application with a GUI that contains three buttons labeled “This,” “That,” and “Exit.” In addition, it contains a text field that initially displays the text, “Text input and output area”

In this example the buttons are only for display. You can click each one, but the program won’t do anything. The text field won’t be changed by the program after it’s initialized either. In the next subsection we’ll add actions to each button and make the program more useful.

Program 15.2 Creates a frame with a panel containing three buttons.
import javax.swing.*;

public class FrameWithPanel {
    public static void main(String[] args){
        JFrame frame = new JFrame("Button Example"); (1)
        JPanel panel = new JPanel(); (2)
		
        JButton thisButton = new JButton("This"); (3)
        JButton thatButton = new JButton("That");
        JButton exitButton = new JButton("Exit");
        JTextField field = new JTextField("Text input and output area");
        
		panel.add(thisButton); (4)
        panel.add(thatButton); 
        panel.add(field); 
        panel.add(exitButton); 
        frame.add(panel); (5)
        frame.setSize(350,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true); (6)
    }  
}
1 We first create a frame named frame.
2 Then, we make a panel named panel.
3 Next, we create three buttons named thisButton, thatButton, and exitButton and a text field named field.
4 We add the buttons and the text field to panel.
5 We add panel to frame.
6 After setting the size and the closing operation, the frame is made visible.

The final GUI is shown in Figure 15.3. The sequence in which you add the buttons to the panel determines the appearance of the GUI. That’s why the text field appears before the “Exit” button. Note that the same GUI may look different on different platforms.

frameWithAPanelFigure
Figure 15.3 GUI consisting of a frame, a panel, three buttons, and a text field.

In addition to JTextField, JTextArea and JPasswordField are two other components useful for entering text. The JTextArea component is designed for larger passages of text and allows multiple lines. The JPasswordField component functions the same as a JTextField except that each character of input text is displayed as a meaningless echo character designed to hide sensitive information like a password.

Figure 15.4(a) shows a GUI with a JTextArea, and Figure 15.4(b) shows a GUI with a JPasswordField.

textAreaAndPasswordFieldFigure
Figure 15.4 (a) GUI with a text area. (b) GUI with a password field.

15.3.3. Adding actions to components

We can design a GUI so that when a user clicks a button, the program responds by printing a message to the terminal, changing a text field, playing a sound, or any other action that a Java program can perform. Clicking a button generates an event. In Swing, an event is processed by one or more listeners. Java provides various types of listeners, some of which are introduced here. Next, we show you how to write listeners to handle events generated by a few different kinds of components.

The ActionListener interface

Java provides an ActionListener interface. This interface has a single method named actionPerformed(). This method takes an ActionEvent as input and performs a suitable action based on the event. A JButton object generates ActionEvent when it’s pressed. Any class that implements the ActionListener interface can be registered as an action listener on a JButton or any other component that generates an ActionEvent.

As discussed in Chapter 10, an interface is a set of method signatures. If a class implements an interface, it promises that its objects will have all of the methods in that interface. If a class implements ActionListener, it’s saying that it knows what to do when an action is performed. The following statements show how to add an ActionListener to a button and implement its actionPerformed() method.

JButton thisButton = new JButton("This");
thisButton.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		// Code to perform an action goes here
	}
});

The first line above creates a button named thisButton. The remaining code adds an action listener to the button. The process of adding an action listener to an object is also known as registering a listener on the object. Note that the sole argument to this addActionListener() method is a newly created ActionListener object. Inside this newly created and anonymous ActionListener object, we implement the actionPerformed() method. Whatever code we want to execute in response to the clicking of the thisButton button goes inside the actionPerformed() method.

This syntax may look strange to you. ActionListener is an interface, which can’t be instantiated. What’s that new keyword doing? It’s doing something pretty amazing by creating an instance of an anonymous class. On the fly, we’re creating a class that has never existed before. It doesn’t even have a name! All we know about it is that it implements the interface ActionListener.

Note that there are braces after the constructor call, defining what’s inside of this class. Inside, we have only created an actionPerformed() method, but we could have created fields as well as other methods. It’s a little ugly to create a whole new class and instantiate it in the middle of calling the addActionListener() method, but it’s also very convenient. We need to supply an object that reacts to the event exactly the way we want it to. Since one doesn’t exist yet, we have to create it. Of course, it’s possible to supply any object that implements the ActionListener interface, not just instances of anonymous classes. For more information about nested classes, inner classes, and anonymous classes, refer to Section 9.4 and Section 10.4.

Example 15.3 GUI with actions

Now we can modify Program 15.2 to respond to button clicks. When the thisButton button is clicked, the program will display the message “You can get with this.” in the text field. Similarly, when the thatButton button is clicked, the program will display “Or you can get with that.”

Program 15.3 Demonstrates handling of action events.
import javax.swing.*;
import java.awt.event.*;

public class FrameWithPanelAndActions {
    public static void main(String[] args){
        JFrame frame = new JFrame("Button Example"); 
        JPanel panel = new JPanel();
		
        JButton thisButton = new JButton("This"); 
        JButton thatButton = new JButton("That"); 
        JButton exitButton = new JButton("Exit"); 
        JTextField field = new JTextField("Text input and output area");
		
        panel.add(thisButton); 
        panel.add(thatButton);
        panel.add(field);
        panel.add(exitButton);
		
        // Add action listeners to various buttons
        thisButton.addActionListener(new ActionListener() { 
            public void actionPerformed(ActionEvent e){
                field.setText("You can get with this.");
            }
        });
        thatButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e){
                field.setText("Or you can get with that.");
            }
        });
        exitButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e){
                System.out.println("Exit");
                frame.dispose();
            }
        });
        frame.add(panel);
        frame.setSize(350,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);       
    }  
}

Program 15.3 is largely the same as Program 15.2. It adds the same buttons and text field but then adds an action listener to each button. The action performed when the thisButton and thatButton buttons are clicked is to display a message in the text box. When the exitButton button is clicked, the listener displays a message on the terminal and exits the program.

The action listeners can be added either before or after the panel has been set up but should be added before the frame is made visible.

Example 15.4 GUI with alternate action listener style

In the previous example, we added an ActionListener object to each button and implemented its actionPerformed() method with anonymous inner classes. An alternate way to use ActionListener is to implement ActionListener on the surrounding class and include an actionPerformed() method exactly once instead of creating several individual anonymous inner classes which each handle an event. Let’s examine one such implementation in Program 15.4 and contrast it with Program 15.3. Note that both programs generate the same GUI and exhibit identical behavior.

Program 15.4 Demonstrates handling of action events by implementing ActionListener at the class level.
import javax.swing.*;
import java.awt.event.*;

public class AlternateActionListener implements ActionListener {
	private JFrame frame = new JFrame("Button Example"); 
	private JPanel panel = new JPanel();	
	private JButton thisButton = new JButton("This"); 
	private JButton thatButton = new JButton("That"); 
	private JButton exitButton = new JButton("Exit"); 
	private JTextField field = new JTextField("Text input and output area");
  
	public AlternateActionListener (){ (1)
		thisButton.addActionListener(this); 
		thatButton.addActionListener(this);
		exitButton.addActionListener(this);

		panel.add(thisButton);
		panel.add(thatButton);
		panel.add(field);
		panel.add(exitButton);    
		frame.add(panel);

		frame.setSize(350,200);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		frame.setVisible(true);       
	}     

	public void actionPerformed(ActionEvent e){ (2)
		Object button = e.getSource(); (3)
		if(button == thisButton) (4)
			field.setText("You can get with this."); 
		else if(button == thatButton) 
			field.setText("Or you can get with that.");
		else {
			System.out.println("Exit");
			frame.dispose();
		}
	}

	public static void main(String[] args){ (5)
		new AlternateActionListener(); 
	}   
}
1 The constructor adds an ActionListener to each button. The listener added is this, specifying that the AlternateActionListener object is the one that will process any action event generated by the buttons. The remainder of the code for the constructor is essentially the same as that from the main() method in Program 15.3.
2 A class that implements ActionListener must include an actionPerformed() method. It has exactly one parameter, an ActionEvent object. Whenever an action event occurs, its attributes are bundled into an ActionEvent object and passed into the actionPerformed() method.
3 The getSource() method returns a reference to the object that generated the event. Variable button holds the object returned by getSource().
4 These if statements compare button with thisButton and thatButton to determine if either of these generated the event. Then, a suitable message is displayed in the field text field. If neither button generated the event, it must have been exitButton, so the frame disposes itself after displaying “Exit” on the terminal.
5 The main() method creates an instance of AlternateActionListener and finishes. The program doesn’t end there because the threads of the GUI are already running.

There are a few differences between Program 15.3 and Program 15.4. In the second program, most of the code has been moved from the main() method to the constructor. Various objects, namely the frame, the panel, all three buttons, and the text box are now fields of the object instead of local variables in the main() method. The buttons need to be fields so that the actionPerformed() method can compare against them, and the text box needs to be a field so that the method can change it.

We’ve examined two styles of adding an ActionListener to a Java program. The choice of style depends on your needs. Adding an anonymous ActionListener to each component makes it easy to specify an action for each one, but the syntax is ugly. Using a named class (often the main program class or a subclass of JFrame) as the ActionListener allows you to handle many events in a centralized location. It can be easier to find errors when all events are handled in one actionPerformed() method, but the method can become complex since it needs to determine which component generated the event.

The ItemListener interface

An ItemListener can be attached to a component such as a check box or a radio button to listen for when it’s selected or deselected. When you select a check box, a ✓ sign appears to its left, and an ItemEvent is generated. When you select an already checked check box, the sign disappears, and another ItemEvent is generated.

Creating a check box is very much like creating a button, except that we use the JCheckBox component instead of JButton. When it’s initially created, a check box is unchecked.

JCheckBox checkBox = new JCheckBox("Check the oven");

Just as the ActionListener interface only has a single method, the ItemListener interface only has one as well, itemStateChanged(), which takes a single parameter of type ItemEvent.

An ActionEvent and an ItemEvent are similar, but one reason that Java has two different interfaces with two different kinds of events is because an ItemEvent has more information: By using the getStateChange() method, it’s possible to tell whether the component that fired the ItemEvent is now selected or deselected.

checkBox.addItemListener(new ItemListener(){
	public void itemStateChanged(ItemEvent e) {
		if(e.getStateChange() == ItemEvent.SELECTED)
			checkBox.setText("We checked the oven!");
		else
			checkBox.setText("No one checked the oven!");
	}
});

Below is an example of a GUI with three check boxes.

Example 15.5 GUI with check boxes

The following program shows a GUI with three check boxes labeled “Nasty”, “Brutish”, and “Short”. When each is clicked, it’ll update a text field saying that your life either is or isn’t nasty, brutish, or short, depending on whether or not the clicked check box is currently selected.

import javax.swing.*;
import java.awt.event.*;

public class CheckBoxExample {
    public static void main(String[] args){
        JFrame frame = new JFrame("Check Box Example"); (1)
        JPanel panel = new JPanel();
		
        JCheckBox nastyCheckBox = new JCheckBox("Nasty"); (2)
		JCheckBox brutishCheckBox = new JCheckBox("Brutish"); 
		JCheckBox shortCheckBox = new JCheckBox("Short"); 		
        JTextField field = new JTextField("Here's what your life is like.");
		
        panel.add(nastyCheckBox); (3)
        panel.add(brutishCheckBox);
        panel.add(shortCheckBox);
		panel.add(field);
		
        // Add item listeners to the check boxes
        nastyCheckBox.addItemListener(new ItemListener() { (4)
            public void itemStateChanged(ItemEvent e) {
				if(e.getStateChange() == ItemEvent.SELECTED)
					field.setText("Your life is nasty.");
				else
					field.setText("Your life isn't nasty.");
            }
        });
        brutishCheckBox.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if(e.getStateChange() == ItemEvent.SELECTED)
					field.setText("Your life is brutish.");
				else
					field.setText("Your life isn't brutish.");
            }
        });
        shortCheckBox.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if(e.getStateChange() == ItemEvent.SELECTED)
					field.setText("Your life is short.");
				else
					field.setText("Your life isn't short.");
            }
        });
        frame.add(panel);  (5)
        frame.setSize(350,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);       
    }  
}
1 Just as in previous examples, we create a frame and a panel.
2 We make three check boxes and a text field.
3 Then we add all four components to the panel.
4 We create an ItemListener for each check box to update the text field with an appropriate message based on whether the check box is now checked or unchecked. Note that the most recent message will overwrite whatever text was previously in the text field.
5 Like previous examples, we add the panel to the frame, set the size, set the default close operation, and show the frame.

Figure 15.5 shows this GUI after the “Brutish” check box has been clicked.

checkBoxExampleFigure
Figure 15.5 GUI consisting of a frame, a panel, three check boxes, and a text field.

All JCheckBox objects could have an ActionListener added to them as well. We could change the listener for the nastyCheckBox above to the following, and it would function the same. To do so, we must ask the nastyCheckBox if it’s currently selected by calling its isSelected() method.

		nastyCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
				if(nastyCheckBox.isSelected())
					field.setText("Your life is nasty.");
				else
					field.setText("Your life isn't nasty.");
            }
        });

Another Swing component that can be used with an ItemListener is JRadioButton. Radio buttons are similar to check boxes in that clicking one will select it. However, radio buttons are almost always used in a group. When one radio button in the group is selected, all others are deselected, guaranteeing that only one radio button can be selected at a time.

For example, the following code will create three radio buttons, only one of which can be selected.

JRadioButton smarterButton = new JRadioButton("Smarter than the average bear");
JRadioButton lessSmartButton = new JRadioButton("Less smart than the average bear");
JRadioButton asSmartButton = new JRadioButton("As smart as the average bear");

ButtonGroup group = new ButtonGroup();
group.add(smarterButton);
group.add(lessSmartButton);
group.add(asSmartButton);

The ButtonGroup object provides the logical grouping that guarantees a maximum of one JRadioButton will be selected. This grouping is only for the purposes of selection, and the radio buttons still need to be added to a panel or a frame or some other GUI container to display them.

With check boxes, an ItemListener didn’t provide much of an advantage over an ActionListener. A radio button, however, can be deselected not by clicking on it but by clicking on another radio button. Thus, using an ItemListener allows us to handle the event when a radio button is deselected, even though it wasn’t clicked itself, as illustrated in the following example.

Example 15.6 GUI with radio buttons

The program below shows a GUI with three radio buttons labeled “Half empty”, “Half full”, and “Twice the needed size”, describing a partially filled glass of water from the perspective of an optimist, a pessimist, or an engineer, respectively. When each radio button is selected, it’ll update the first text field describing the glass from the given perspective. However, when a radio button is deselected by clicking another one, it’ll update the second text field to describe what the glass isn’t, now that we’re in a different perspective.

import javax.swing.*;
import java.awt.event.*;

public class RadioButtonExample {
    public static void main(String[] args){
        JFrame frame = new JFrame("Radio Button Example");
        JPanel panel = new JPanel();
		
        JRadioButton halfEmptyButton = new JRadioButton("Half empty"); (1)
		JRadioButton halfFullButton = new JRadioButton("Half full"); 
		JRadioButton twiceTheSizeButton = new JRadioButton("Twice the needed size"); 		
        JTextField positiveField = new JTextField("Positive statement about the glass.");
		JTextField negativeField = new JTextField("Negative statement about the glass.");
		
		ButtonGroup group = new ButtonGroup(); (2)
		group.add(halfEmptyButton);
		group.add(halfFullButton);
		group.add(twiceTheSizeButton);		
		
        panel.add(halfEmptyButton); (3)
        panel.add(halfFullButton);
        panel.add(twiceTheSizeButton);
		panel.add(positiveField);
		panel.add(negativeField);
		
        // Add item listeners to the radio buttons
        halfEmptyButton.addItemListener(new ItemListener() { (4)
            public void itemStateChanged(ItemEvent e) {
				if(e.getStateChange() == ItemEvent.SELECTED)
					positiveField.setText("The glass is half empty.");
				else
					negativeField.setText("And it's not half empty.");
            }
        });
        halfFullButton.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if(e.getStateChange() == ItemEvent.SELECTED)
					positiveField.setText("The glass is half full.");
				else
					negativeField.setText("And it's not half full.");
            }
        });
        twiceTheSizeButton.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if(e.getStateChange() == ItemEvent.SELECTED)
					positiveField.setText("The glass is twice the needed size.");
				else
					negativeField.setText("And it's not twice the needed size.");
            }
        });
        frame.add(panel);  (5)
        frame.setSize(350,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);       
    }  
}
1 We make three radio buttons and two text fields.
2 We add the three radio buttons to a group.
3 Then we add the five components to the panel.
4 We create an ItemListener for each radio button. When the event is a selection, it will update the first text field with a message that matches the radio button. When the event is a deselection, it will update the second text field with a message that suggests the opposite of the radio button.
5 Like previous examples, we add the panel to the frame, set the size, set the default close operation, and show the frame.

Figure 15.6(a) shows this GUI before any radio buttons have been selected. Figure 15.6(b) shows this GUI after the “Half full” radio button’s been selected. Figure 15.6(c) shows this GUI after the “Twice the needed size” radio button’s been selected. In this final step, note that the second text field reads “And it’s not half full.” because the “Half full” button was just deselected.

radioButtonExampleFigure
Figure 15.6 GUI with radio buttons in three different states.
The MouseListener interface

Clicking a button or a check box is useful, but a mouse can generate events in other ways too. For example, in a screen full of pictures, you might want to highlight a picture when the cursor hovers over it. Or you might want to create a drawing program which uses a mouse as a pen. To process general mouse events, we need an object that implements the MouseListener interface, which defines the following methods.

  • mouseClicked()

  • mouseEntered()

  • mouseExited()

  • mousePressed()

  • mouseReleased()

The function of each method is implied by its name. The mouseEntered() event fires when the mouse cursor moves into the area above a component. Conversely, the mouseExited() event fires when a mouse cursor was over a component and has just moved away. The mousePressed() event fires when a mouse button is pressed over a component. The mouseReleased() event fires when a mouse button is released over a component. The mouseClicked() event is a combination of both the mousePressed() and mouseReleased() events, occurring only if a mouse button was pressed and then released while the cursor was over a component. As you can see, a component only fires events when the cursor is over it (or has just left). Thus, a component only reports events that have to do with it, not the general state of the mouse.

Each method in MouseListener receives a MouseEvent object as its argument. To handle mouse events, a class must implement the MouseListener interface. Doing so is similar to the implementation of the ActionListener interface from the previous section, but implementing MouseListener requires a definition for each of the five methods listed above. The next example illustrates MouseListener in use.

Example 15.7 Mouse listener

We can write a program that displays a GUI containing two buttons labeled “One” and “Two.” A text box will display a suitable message when the cursor enters a button. When a button is clicked, the text box should display the total number of times that button has been clicked.

Program 15.5 Demonstrates the handling of mouse generated events using the MouseListener interface.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class SimpleMouseEvents implements MouseListener { (1)
    private JFrame frame = new JFrame("Mouse Events");
    private JTextField status = new JTextField("Mouse status comes here.");
    private JButton oneButton = new JButton("One");
    private JButton twoButton = new JButton("Two");
    private int oneClicks = 0, twoClicks = 0; // Number of clicks
    
    public SimpleMouseEvents() { (2)
        JPanel panel = new JPanel();        
        oneButton.addMouseListener(this);
        twoButton.addMouseListener(this);
        panel.add(oneButton);
        panel.add(twoButton);
        panel.add(status); 
        frame.add(panel);
        frame.setSize(275,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true); 
    }

    // Implement all abstract methods in MouseListener
    public void mouseEntered(MouseEvent e) { (3)
        if (e.getSource() == oneButton) 
            status.setText("Mouse enters One.");
        else
            status.setText("Mouse enters Two.");      
    }
    
    public void mouseClicked(MouseEvent e) { (4)
        if (e.getSource() == oneButton) {
            oneClicks++;
            status.setText("One clicked "+ oneClicks + " times.");
        }           
        else {
            twoClicks++;
            status.setText("Two clicked "+ twoClicks  + " times.");
        }
    }

    public void mouseExited(MouseEvent e) {} // Unused methods (5)
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {} 
    
    public static void main(String[] args){ (6)
          new SimpleMouseEvents();
    }  
}
1 We declare class SimpleMouseEvents, implementing the MouseListener interface. The following few lines declare frame frame, buttons one and two, and a text box status. Two integers oneClicks and twoClicks are initialized to 0 and are used to keep track of the number of times each button has been clicked.
2 The first line of the SimpleMouseEvents constructor creates the panel panel. It doesn’t need to be a field, and it’s always preferable to keep a variable local if it can be. The next two lines add a MouseListener to the two buttons. Note the use of this in the argument to addMouseListener() which refers to the object being created by the constructor. Next, the panel is set up and added to the frame. Finally the frame size and its default close operations are set, and the frame is made visible.
3 The mouseEntered() method is invoked when the cursor enters either of the two buttons. First, we retrieve the source of the event using the getSource() method and identify which object generated the event. A suitable message is displayed in the status box using the setText() method.
4 The mouseClicked() method is invoked when the mouse cursor is clicked above a button. As before, we retrieve the source of the event using the getSource() method. A suitable message, including the number of clicks, is displayed in the text box. Of course, recording button clicks could have been done with an ActionListener instead.
5 MouseListener methods that aren’t used must be implemented, but their bodies can be left empty.
6 The only job of the main() method is to create an instance of SimpleMouseEvents.

Program 15.5 generates the GUI shown in Figure 15.7.

simpleMouseEventsFigure
Figure 15.7 GUI consisting of two buttons and a text box. Button clicks and entry of the cursor into a button are reported by the text box.
Mouse adapter

Creating a MouseListener requires all five methods in the interface to be implemented. In many cases, as in Example 15.7, there’s no need to implement all the methods because we’re not interested in all the corresponding events. In such situations we’re forced to include empty methods. However, you might want to include the methods only when they’re needed. The MouseAdapter class helps us avoid implementing methods we don’t need.

MouseAdapter is an abstract class, unlike the MouseListener interface. The advantage of using MouseAdapter is that it already provides a skeletal implementation of each method needed to process mouse events. We can override these implementations as needed, and we don’t need to provide an implementation of a method that’s not used.

Example 15.8 Mouse adapter

Program 15.6 is a revised version of Program 15.5. Remember that an abstract class is extended whereas an interface is implemented.

Program 15.6 Handles mouse generated events using the MouseAdapter abstract class.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class SimpleMouseAdapter extends MouseAdapter { (1)
    private JFrame frame = new JFrame("Mouse Events");    
    private JTextField status = new JTextField("Mouse status comes here.");
    private JButton oneButton = new JButton("One");
    private JButton twoButton = new JButton("Two");
    private int oneClicks = 0, twoClicks = 0; 
    
    public SimpleMouseAdapter () {
        JPanel panel = new JPanel();
        oneButton.addMouseListener(this);
        twoButton.addMouseListener(this);
        panel.add(oneButton);
        panel.add(twoButton);
        panel.add(status); 
        frame.add(panel);
        frame.setSize(275,200);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true); 
    }
    
    // Override only those methods we want
    public void mouseEntered(MouseEvent e) { (2)
        if (e.getSource() == oneButton)
            status.setText("Mouse enters One.");
        else
            status.setText("Mouse enters Two.");      
    }
    
    public void mouseClicked(MouseEvent e) { (3)
        if (e.getSource() == oneButton) {
            oneClicks++;
            status.setText("One clicked "+ oneClicks + " times.");
        }           
        else {
            twoClicks++;
            status.setText("Two clicked "+ twoClicks  + " times.");
        }
    }
    
    public static void main(String[] args){
        new SimpleMouseAdapter ();
    }  
}
1 Class SimpleMouseAdapter extends the abstract class MouseAdapter. Thus, it inherits all the empty methods defined in MouseAdapter.
2 We override the method mouseEntered().
3 We override the method mouseClicked(). No other methods need to be overridden in this example.
Other event listeners

In this chapter we describe three types of listeners in Java, ActionListener, ItemListener and MouseListener. You may have noticed that none of the mouse events we discussed involved the movement of the mouse inside of the component, only whether it was entering or exiting the component. Because tracking mouse movement is more computationally expensive than tracking simple presses, releases, enters, and exits, Java uses yet another listener to handle mouse movement, MouseMotionListener. It contains the methods mouseDragged() and mouseMoved(), which are used to handle mouse movement with or without the button pressed.

Java provides several other listeners to handle a variety of events. For example, the DocumentListener can be attached to a JTextField or a JTextArea object to listen to document events, which include the insertUpdate() event that’s fired when a character is inserted the text box. A KeyListener can also be attached to text boxes to listen to key events such as the return key being typed, which can have similar functionality. These events could be useful when developing a text editor application, for example.

After you’ve mastered the contents of this chapter, you may plan to write more complex GUIs than the ones we discuss. For further information, you might want to follow the Java tutorial on writing event listeners at the Oracle Swing Events tutorial site.

15.3.4. Adding sounds and images

Sounds and images can also be added to a Java GUI application. While Java offers a rich set of sound APIs, we restrict our examples to playing sound clips from audio files that come in au or wav formats. We also introduce the ImageIcon class to create icons from image files.

Sounds

There are many ways to play sounds in Java, but the simplest is through the Clip interface. Let’s see how we can create an appropriate Clip object.

Clip clip = AudioSystem.getClip();

This statement creates a new Clip object using a static method from the AudioSystem class in the javax.sound.sampled package. To use clip, we need an audio file to play.

File soundFile = new File("sounds/trumpet.wav");       (1)
clip.open(AudioSystem.getAudioInputStream(soundFile)); (2)
1 Here we create a File object corresponding to the location of an audio file we want to play. In this case, we’re trying to play a file called trumpet.wav in a sounds directory that’s in the same directory as our program. The location of the file could be much more complicated or even entered by the user.
2 Then, we use the AudioSystem class to make an audio stream from the file and open it with clip. Note that the two methods on this line can throw a number of exceptions, depending on whether the file is in a usable format and your computer’s audio system is ready.
clip.start();

This command will play the clip in clip loaded from the specified file, exactly once. If you want to play the clip in a loop, use the loop() method. You can specify a specific number of times to loop or use the Clip.LOOP_CONTINUOUSLY constant to make it loop an unlimited number of times.

clip.loop(Clip.LOOP_CONTINUOUSLY);

To stop a clip from playing, use the stop() method.

clip.stop();

Now that we’ve seen how to create, open, play, and stop an audio clip, we’re ready to write a program that can play sounds.

Example 15.9 Animal sounds

We can write a program to play a loop of a bird chirping or a dog barking when the corresponding button is clicked. Our program only has two sounds, but more could be added.

animalSoundsFigure
Figure 15.8 GUI for the animal sounds application. (a) On program start. (b) After the “Chirp” button has been clicked and the clip is playing.

We can include a button labeled “Stop Sound” that stops the playback of sounds when clicked. When it starts, the GUI will look like Figure 15.8(a). Note that the “Stop Sound” button is gray, showing that it’s disabled. The complete program is shown below.

Program 15.7 Animal sounds
import java.io.*;  (1)
import java.awt.event.*;
import javax.swing.*;
import javax.sound.sampled.*;

public class AnimalSounds { 
  public static void main (String[] args) throws Exception {
    JFrame frame = new JFrame("Animal Sounds");
    JPanel panel = new JPanel();
    JButton chirpButton = new JButton("Chirp");
    JButton barkButton = new JButton("Bark");
    JButton stopButton = new JButton("Stop Sound");
    JTextField field = new JTextField("Click Chirp or Bark.");
    File chirpFile = new File("sounds/chirp.wav"); (2)
    File barkFile = new File("sounds/bark.wav");
    Clip chirpClip = AudioSystem.getClip(); (3)
	chirpClip.open(AudioSystem.getAudioInputStream(chirpFile));
    Clip barkClip = AudioSystem.getClip();
	barkClip.open(AudioSystem.getAudioInputStream(barkFile));
    panel.add(chirpButton);
    panel.add(barkButton);
    panel.add(stopButton);
    panel.add(field);
    frame.add(panel);
    stopButton.setEnabled(false); (4)
1 Many import statements are needed to cover all the library classes and interfaces used to play sounds.
2 After creating a number of GUI elements, we define the files for the two sounds.
3 Then, we create chirpClip and barkClip and open audio streams corresponding to their sound files.
4 Note that we start with the stop button disabled.
    chirpButton.addActionListener(new ActionListener() { (1)
        public void actionPerformed(ActionEvent e){
            field.setText("Playing chirp.");
            barkButton.setEnabled(false);
            chirpClip.loop(Clip.LOOP_CONTINUOUSLY);
            stopButton.setEnabled(true);
      }
    });
    barkButton.addActionListener(new ActionListener(){ (2)
        public void actionPerformed(ActionEvent e){
            field.setText("Playing bark.");
            chirpButton.setEnabled(false);
            barkClip.loop(Clip.LOOP_CONTINUOUSLY);
            stopButton.setEnabled(true);
      }
    });
    stopButton.addActionListener(new ActionListener(){ (3)
        public void actionPerformed(ActionEvent e){
            field.setText("Click Chirp or Bark.");
            chirpClip.stop();            
            barkClip.stop();
			barkButton.setEnabled(true);
            chirpButton.setEnabled(true);
            stopButton.setEnabled(false);
      }
    });

    frame.setSize(275,200); // Set size in pixels
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.setVisible(true); // Display it      
  }
}
1 Here we define the action listener for chirpButton, which disables barkButton, tells the user that the chirp sound is playing, plays the sound, and enables the stop button.
2 The action listener for barkButton button is similar.
3 The action listener for stopButton doesn’t know which sound is playing, so it stops both sounds and then disables the stopButton.

When chirpButton is clicked, the GUI looks like Figure 15.8(b).

Images and icons

Images are often useful when creating GUIs. In this section we show you how to use images to create icons and then use those icons to decorate buttons and labels. First, let’s see how an icon object can be created. Suppose we have a picture file named smile.jpg in a directory named pictures. Note that the pictures directory should be located in same directory as the class files for your program. The following statement creates an object of type ImageIcon from this picture.

ImageIcon smileIcon = new ImageIcon("pictures/smile.jpg");

The file name, along with its path, is passed to the ImageIcon constructor as a string. Now we can add the image to a button.

JButton smile = new JButton();
smile.add(smileIcon);

Similarly, you can add an image to a label. The next example gives a simple program that creates a button with an image.

Example 15.10 Icon example

Figure 15.9 shows a GUI with a button decorated with a picture. Program 15.8 gives the code to create this GUI using the ImageIcon class.

iconExampleFigure
Figure 15.9 A GUI with a button decorated by an image icon.
Program 15.8 Creates a GUI with a button decorated by a picture.
import javax.swing.*;
import java.awt.*;

public class IconExample {   
    public static void main(String[] args) {
        JFrame frame = new JFrame("Icon Example");     
        ImageIcon smileIcon = new ImageIcon("pictures/smile.jpg"); (1)
        JButton smileButton = new JButton(smileIcon); (2)
        frame.add(smileButton);
        frame.setSize(325,250);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);
  }  
}
1 An image icon is created from a JPEG file named smile.jpg located in the pictures directory.
2 We create a button named smileButton and decorate it with an icon by supplying it as an argument to the JButton constructor. If the file can’t be found, the program will fail quietly. This means that no exception is thrown. Instead, the button will appear without an image.
Labels, icons, and text

In some applications you might want to show a picture with an attached text label. For example, a shopping cart application for an online clothing store might shows pictures of clothes, each labeled with a name and a price. The JLabel class is flexible, able to display text alone, an image alone, or both. However, a JLabel is designed for displaying information and can’t read user input.

Here are three different ways to create a label.

ImageIcon hibiscus = new ImageIcon("pictures/hibiscus.jpg")
JLabel textOnly = new JLabel("Text only");
JLabel flower = new JLabel(hibiscus);
JLabel labeledflower = new JLabel("Red Hibiscus", hibiscus, JLabel.CENTER);

The first JLabel constructor above creates a label displaying only text, namely, “Text only.” The second constructor creates a label decorated with an icon created from a picture. The third constructor creates a label with the same icon and additional text. The last argument in this third case is JLabel.CENTER, a constant that specifies that the content of the label (both the image and the text) should be placed horizontally in the center of the label. A horizontal alignment of left or right could also be specified using the constants JLabel.LEFT or JLabel.RIGHT, respectively.

Sometimes you might want to place the text below the icon that decorates the label. To do so, you could set the horizontal and vertical positions of the text as follows.

flower.setVerticalTextPosition(JLabel.BOTTOM);
flower.setHorizontalTextPosition(JLabel.CENTER);
Example 15.11 Label example

Figure 15.10(a) is generated by Program 15.9.

labelExampleFigure
Figure 15.10 (a) A GUI with a label decorated by an image icon and a title beneath it. (b) The GUI with unaligned text.
Program 15.9 Creates a GUI with a label decorated by a picture and text.
import javax.swing.*;
import java.awt.*;

public class LabelExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Label Example");
        ImageIcon hibiscusIcon = new ImageIcon("pictures/hibiscus.jpg");
        JLabel flower = new JLabel("Red Hibiscus", hibiscusIcon, JLabel.CENTER);
        flower.setVerticalTextPosition(JLabel.BOTTOM); (1)
        flower.setHorizontalTextPosition(JLabel.CENTER); (2)
        frame.add(flower);
        frame.setSize(300,250);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);
  }  
}
1 Vertically position the text on the bottom of the label.
2 Horizontally center the text on the label.

You will see the GUI shown in Figure 15.10(b) if the alignment instructions on these two lines are omitted.

15.3.5. Layout managers

Java provides a number of layout managers to assist with the design of GUIs. A layout manager controls the placement of components on a frame or panel. Every container has a default layout manager, but it’s possible to change that manager to a different one. In this section we’ll introduce the three layout managers FlowLayout, GridLayout, and BorderLayout. Java also provides several other layout managers, each designed for different situations.

FlowLayout

The FlowLayout manager is one of simplest. When a container is using the FlowLayout manager, components will be added in order from left to right. When there’s no more space, subsequent components will be added starting on the next row. In addition, each row of components is centered within the container. The JPanel container uses FlowLayout by default, but it’s possible to set it explicitly as well.

JPanel panel = new JPanel(new FlowLayout());

When we’ve added more than one component to a JFrame in previous examples, we’ve first added them to a JPanel. The reason we did so is because FlowLayout is the default layout manager for JPanel containers. Although every JFrame has a container, it uses the BorderLayout manager by default, which would have complicated our earlier examples. The next example illustrates FlowLayout further.

Example 15.12 FlowLayout

Program 15.10 creates a GUI with several buttons.

Program 15.10 Adds several buttons using FlowLayout.
import javax.swing.*;
import java.awt.*;

public class FlowLayoutExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("FlowLayout Example"); (1)
        JPanel panel = new JPanel(new FlowLayout()); (2)
        final int MAX_BUTTONS = 6;    
        for(int i = 0; i < MAX_BUTTONS; i++) (3)
            panel.add(new JButton("   " + i + "   "));
        frame.add(panel);
        frame.setSize(300,200);    
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);    
        frame.setResizable(false);
        frame.setVisible(true);       
    }  
}
1 We first create a frame.
2 We create a panel and set its layout manager to FlowLayout.
3 This for loop is used to create new buttons, label them appropriately, and add them to the panel.

The FlowLayout manager places the buttons along a number of rows depending on the width of the frame. If the container changes size, the components will react by "'flowing'" into different rows, hence the name. The GUI created is shown in Figure 15.11.

flowLayoutFigure
Figure 15.11 Using the FlowLayout manager to generate a GUI containing six buttons.
GridLayout

The GridLayout manager lays out components in a grid with a set number of rows and columns. As with other layout managers, GridLayout can be applied to frames and panels.

JFrame frame = new JFrame("Grid Layout Example");
frame.setLayout(new GridLayout(3, 2, 5, 5));

This snippet creates a frame named frame and sets its layout manager to GridLayout. The first two arguments to GridLayout give the number of rows and columns, respectively. The last two arguments (which are optional) give the horizontal and vertical gaps between the neighboring cells in the grid. In this example the frame will contain a total of six cells organized into three rows with two columns.

gridLayoutFigure
Figure 15.12 A 3 × 2 grid layout containing six buttons.

Figure 15.12 shows how frame will look after six buttons, labeled 0 through 5, have been added to it in order. The buttons were created in the same way as the buttons in Program 15.10, but they look different because of the GridLayout manager. A key feature of using GridLayout is that all cells in the grid will be the same size and will stretch to fill the entire container. Also note the equal spacing between the neighboring cells. It’s possible to add more cells or fewer cells than specified in the GridLayout constructor, but the layout manager will be forced to guess at your intentions.

Example 15.13 Animal identifier

We can write a program to display pictures of animals and identify which animal the mouse is current hovering over. The animal’s name will be displayed in the title of the frame. Figure 15.13 shows this GUI.

animalIdentifierFigure
Figure 15.13 A GUI for an animal identifier. Note that the bison is identified according to the frame title.

Program 15.11 creates the GUI shown in Figure 15.13.

Program 15.11 Identifies which animal is in various images when the mouse hovers over the image. Images are laid out using a GridLayout manager.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class AnimalIdentifier extends MouseAdapter {  (1)
    private JLabel bison, dove, gecko, spider; (2)
    private JFrame frame = new JFrame("Animal Identifier");

    public AnimalIdentifier() { 
        JPanel panel = new JPanel(new GridLayout(2,2,5,5)); (3)
        ImageIcon bisonIcon = new ImageIcon("pictures/bison.jpg");
        ImageIcon doveIcon = new ImageIcon("pictures/dove.jpg");
        ImageIcon geckoIcon = new ImageIcon("pictures/gecko.jpg");
        ImageIcon spiderIcon = new ImageIcon("pictures/spider.jpg");
        bison = new JLabel(bisonIcon); (4)
        bison.addMouseListener(this); 
        dove = new JLabel(doveIcon);
        dove.addMouseListener(this);
        gecko = new JLabel(geckoIcon);
        gecko.addMouseListener(this);
        spider = new JLabel(spiderIcon);
        spider.addMouseListener(this);
        panel.add(bison); (5)
        panel.add(dove); 
        panel.add(gecko);
        panel.add(spider);
        frame.add(panel);
        frame.setSize(400,400); (6)
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true); 
    }
1 Class AnimalIdentifier extends MouseAdapter so that it can be added as a MouseListener without the need to implement all of its methods.
2 We declare four labels and a frame as fields. The labels are named bison, dove, gecko, and spider.
3 Inside the constructor, we create a panel with a 2 × 2 GridLayout. Then, we create four image icons, one to decorate each label.
4 Each of the four labels is created with its respective icon. We also add the AnimalIdentifier object we’re constructing as a MouseListener for each label.
5 The buttons are added to the panel, and the panel is added to the frame.
6 The last few lines set the size of the frame, set its close operation, and make it visible.
	public void mouseEntered(MouseEvent e) { (1)
        Object label = e.getSource(); (2)
        if(label == bison)
          frame.setTitle("Animal Identifier: Bison");
        else if(label == dove)
          frame.setTitle("Animal Identifier: Dove");
        else if (label == gecko)
          frame.setTitle("Animal Identifier: Gecko");    
        else if (label == spider)
          frame.setTitle("Animal Identifier: Spider");
    }

    public static void main(String[] args) { (3)
        new AnimalIdentifier();
    }
}
1 Since AnimalIdentifier extends MouseListener, we only need to implement the single MouseListener method we care about, mouseEntered().
2 We get the label the mouse entered. We compare this label to the four labels and change the frame title correspondingly.
3 The main() method creates a new instance of class AnimalIdentifier, launching the GUI.

Play with Program 15.11. What happens when you resize the window?

BorderLayout

The BorderLayout manager is the default one for a JFrame. It allows components to be laid out spatially in regions of a container. These regions are north, south, east, west, and center. This layout is intuitively easy to understand, but it’s difficult to describe precisely.

You can only add one component to each region of the layout, and adding a component to any region is optional. The regions will stretch or shrink to accommodate the components inside. The north and south regions will only be as tall as needed to hold their contents, but their width will stretch as wide as the entire container. The east and west regions will only be as wide as needed to hold their contents, but their height will stretch as tall as needed to fit the remaining height of the container. Both the height and the width of the center region will stretch as big as it needs to fill the container.

Example 15.14 BorderLayout

Here’s an example of a frame using BorderLayout. Five buttons have been added, one to each region, using the program shown below. Since the default layout manager of a JFrame is a BorderLayout, we don’t need to state it explicitly, although we do in this case.

Program 15.12 Shows buttons laid out in each of the five regions of a BorderLayout.
import javax.swing.*;
import java.awt.*;

public class BorderLayoutExample {
	public static void main(String[] args) {
		JFrame frame = new JFrame("BorderLayout Example");	
		frame.setLayout(new BorderLayout());
		frame.add(new JButton("North"), BorderLayout.NORTH);
		frame.add(new JButton("South"), BorderLayout.SOUTH);
		frame.add(new JButton("East"), BorderLayout.EAST);
		frame.add(new JButton("West"), BorderLayout.WEST);
		frame.add(new JButton("Center"), BorderLayout.CENTER);
		frame.setSize(400,250);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);    
		frame.setVisible(true);       
	}  
}
borderLayoutFigure
Figure 15.14 A GUI demonstrating BorderLayout generated by Program 15.12.

Unlike FlowLayout or GridLayout, the location must be specified to add a component to a BorderLayout. To do so, the add() method takes a second parameter, which is one of the constants BorderLayout.NORTH, BorderLayout.SOUTH, BorderLayout.EAST, BorderLayout.WEST, or BorderLayout.CENTER, depending on where you want to add the component. If you don’t specify a second parameter, the component will be added to the center region. Since only one component can be in each region, adding a component to a region that’s already occupied will replace the old component with the new one.

At first glance, BorderLayout seems that it would rarely be useful. However, this layout is frequently used because it establishes a spatial relationship between different parts of a GUI. A container with a BorderLayout generally has other containers with their own layouts added to its regions, as shown in the following example.

Example 15.15 Calculator layout

We can make a GUI application that functions as a simple calculator. The calculator has the ten digits 0-9, a plus button, a minus button, and an enter button. At the top is a display that shows the current value.

We create ten JButton objects for the digits and three more JButton objects for plus, minus, and enter. The display is a JLabel. The code is given below.

Program 15.13 Lays out a simple calculator.
import javax.swing.*;
import java.awt.*;

public class CalculatorLayout {
	public static void main(String[] args) {
		JFrame frame = new JFrame("Calculator Layout"); 
		frame.setLayout(new BorderLayout());
		JPanel numbers = new JPanel(new GridLayout(4,3));
		numbers.add(new JButton("7"));
		numbers.add(new JButton("8"));
		numbers.add(new JButton("9"));
		numbers.add(new JButton("4"));
		numbers.add(new JButton("5"));
		numbers.add(new JButton("6"));
		numbers.add(new JButton("1"));
		numbers.add(new JButton("2"));
		numbers.add(new JButton("3"));
		numbers.add(new JButton("0"));
		numbers.add(new JButton("+"));
		numbers.add(new JButton("-"));
		frame.add(numbers, BorderLayout.CENTER);
		JButton enter = new JButton("Enter");
		frame.add(enter, BorderLayout.SOUTH);
		JLabel display = new JLabel("0");
		frame.add(display, BorderLayout.NORTH);
		frame.setSize(300,350);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);    
		frame.setVisible(true);       
	}  
}
calculatorLayoutFigure
Figure 15.15 GUI generated by Program 15.13.

This program uses a frame with a BorderLayout to arrange components around a central panel with a GridLayout. First, we put the ten digit buttons in a panel with a GridLayout having 4 rows and 3 columns. We put the plus button and the minus button in the remaining two cells of the grid. We add this grid panel to the center region of the frame. We put the enter button in the south region and the display in the north region.

Note that this program only creates the layout of a simple calculator, not the functionality.

There’s no limit to how deeply you can nest containers within each other. Sometimes you must use many BorderLayout managers to achieve the appearance you want. The value of establishing these spatial relationships is that they’re still valid even when the GUI is resized. Even so, it’s challenging to make a usable GUI that looks attractive no matter how the user resizes it or what the resolution of the user’s screen is.

Although there’s not enough space here to discuss strategies for developing attractive GUIs, it’s worth noting the value of the setPreferredSize() method that all components have. This method is one of the best ways to suggest a size for particular components, especially if other components and containers are going to stretch as needed. When key components have their sizes set using setPreferredSize(), the entire frame can be packed using the pack() method, which sizes the frame to fit its components.

The three layout managers discussed in this section are the simplest, but there are others. The BoxLayout manager is a useful tool for laying out components in a line, whether horizontal like a row or vertical like a column. The GridBagLayout manager can be used to create complex layouts in a single container using a grid-based framework that’s much more flexible than GridLayout, but the complexity of programming GridBagLayout is significant. The SpringLayout and GroupLayout managers are also powerful, but they’re designed for use with a GUI builder utility.

15.3.6. Menus

Menus provide a useful form of interface that’s expected from most GUI applications. In this section we show how to create menus and respond to the selection of menu items. Menus are placed on a menu bar. Each menu usually consists of several menu items that could be selected by the user. In addition to simple text, a menu item can be a button, a radio button, check box, or an icon. A menu can have one or more sub-menus opening out of a menu item.

Creating menus

To use menus we have to create a menu bar.

JMenuBar menuBar = new JMenuBar();

This statement creates an object of type JMenuBar named menuBar which can hold menus. A JFrame has only one menu bar. We can create several menus and add them to the menu bar.

JMenu frenchMenu = new JMenu("French Menu");
JMenu germanMenu = new JMenu("German Menu");
menuBar.add(frenchMenu);
menuBar.add(germanMenu);

These statements create two menus named frenchMenu and germanMenu labeled “French Menu” and “German Menu,” respectively. The two menus can be added to the existing menu bar using the add() method. Menus can be populated with menu items as follows.

JMenuItem coqAuVin = new JMenuItem("Coq au vin");
JMenuItem moulesFrites = new JMenuItem("Moules-frites");
frenchMenu.add(coqAuVin);
frenchMenu.add(moulesFrites);

These statements create two menu items named coqAuVin and moulesFrites. These menu items are then added to the menu frenchMenu. After having created a menu bar together with its menus and their respective menu items, we need a frame so that we can set its menu bar to the one we created.

JFrame frame = new JFrame("Menu Example");
frame.setJMenuBar(menuBar);

These statements create a frame and set its menu bar to menuBar. It’s possible to use the add() method instead of the setJMenuBar() method to add a JMenuBar to a JFrame. However, doing so will add the JMenuBar to the regular content area, not to the menu area.

Figure 15.16 shows a GUI with the menus and menu items added above.

Sometimes you might need to disable a menu item and enable it only under certain conditions.

JMenuItem unusable = new JMenuItem("Currently unusable");
unusable.setEnabled(false);

These statements create a menu item named unusable and disable it. A disabled menu item shows as a gray item and doesn’t respond to attempts to select it. As we showed in Example 15.9, JButton objects, like many other components, can be disabled in the same way.

Adding events to menus

An action listener can be added to each menu item, just like a JButton. Then, when a menu item is clicked by the user, an action event is generated. A JCheckBoxMenuItem object can be added to a JMenu as well. This object will have a check box which can be selected or unselected. A regular JMenuItem object generates an ActionEvent which is handled by an ActionListener. Like a JCheckBox, a JCheckBoxMenuItem can also generate an ItemEvent handled by an ItemListener. Here are examples of both situations.

JMenuItem clickHere = new JMenuItem("Click here");
JCheckBoxMenuItem checkBox = new JCheckBoxMenuItem("Check yourself");
clickHere.addActionListener(this);
checkBox.addItemListener(this);

A JMenuItem works just like a JButton. In fact, the same action listener code could handle events for both buttons and menu items. Similar to JCheckBox objects, a JCheckBoxMenuItem can be handled with an ActionListener if you only want to know when it’s clicked on. However, you can add an ItemListener if you want to write an event handler that runs whenever the state of its check box changes.

15.4. Solution: Math tutor

Here we give the code to generate the GUI shown in Figure 15.1 which displays basic (addition and subtraction) as well as advanced (multiplication and division) problems.

As shown in Figure 15.1, this GUI has a menu bar consisting of two menus labeled “Type” and “Operations.” The “Type” menu contains a check box labeled “Advanced” while the “Operations” menu contains four menu items labeled “Add,” “Subtract,” “Multiply,” and “Divide.” Note that the “Multiply” and “Divide” menu items start disabled. They’ll be enabled when the user selects the “Advanced” check box.

Before we introduce the program that creates this GUI, we need a helper class called ProblemGenerator that can randomly generate arithmetic problems. The class is designed so that the answers are always positive integers.

Program 15.14 Utility class to generate random addition, subtraction, multiplication, and division problems.
import java.util.Random;
import javax.swing.*;

public class ProblemGenerator {
    private Random random = new Random();
    
    public int addPractice(JLabel label) {
        int a = random.nextInt(12) + 1;
        int b = random.nextInt(12) + 1;
        label.setText(a + " + " + b + " = ");
        return a + b;       
    }
    
    public int subtractPractice(JLabel label) {
        int a = random.nextInt(12) + 1;
        int b = a + random.nextInt(12) + 1;
        label.setText(b + " - " + a + " = ");
        return b - a;       
    }
    
    public int multiplyPractice(JLabel label) {
        int a = random.nextInt(12) + 1;
        int b = random.nextInt(12) + 1;
        label.setText(a + " \u00D7 " + b + " = ");
        return a * b;       
    }
    
    public int dividePractice(JLabel label) {
        int a = random.nextInt(12) + 1;
        int b = a*(random.nextInt(12) + 1);
        label.setText(b + " \u00F7 " + a + " = ");
        return b / a;       
    }
}

The code listed above has methods addPractice(), subtractPractice(), multiplyPractice(), and dividePractice(). Each method generates an appropriate math problem, sets an input JLabel to display the problem, and returns the solution. Note that \u00D7 and \u00F7 are the Unicode values for the multiplication (×) and division (÷) symbols.

Program 15.15 generates the GUI in Figure 15.1.

Program 15.15 Uses menus to generate math problems.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class MathTutor implements ActionListener, ItemListener {
    private JMenuItem add = new JMenuItem("Addition"); (1)
    private JMenuItem subtract = new JMenuItem("Subtraction");
    private JMenuItem multiply = new JMenuItem("Multiply");
    private JMenuItem divide = new JMenuItem ("Divide");
    private JLabel score = new JLabel("Score: 0 Correct 0 Incorrect");
    private JLabel label = new JLabel();
    private JTextField field = new JTextField(10);
    private JButton submitButton = new JButton("Submit");
	private ProblemGenerator generator = new ProblemGenerator(); (2)
	private int correct = 0;	(3)
    private int incorrect = 0;
    private int answer = -1;
1 We begin by creating the GUI components that need to interact with event handlers: four menu items, a label, a text field, and a button.
2 We also create a ProblemGenerator object to help create new problems.
3 Next, we add int fields to store the number of correct and incorrect answers as well as the answer to the current problem.
    public MathTutor() { (1)
        JFrame frame = new JFrame("Math Tutor");
        JMenuBar menuBar = new JMenuBar();
        JMenu typeMenu = new JMenu("Type");
        JMenu operationsMenu = new JMenu("Operations");    
        JCheckBoxMenuItem advanced = new JCheckBoxMenuItem("Advanced");
        // Add listeners to menu items (2)
        add.addActionListener(this);
        subtract.addActionListener(this);
        multiply.addActionListener(this);
        divide.addActionListener(this);
		advanced.addItemListener(this); (3)
		//Add anonymous ActionListener to submitButton (4)
		submitButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				int response = Integer.parseInt(field.getText());
				if(response == answer)
					correct++;
				else
					incorrect++;                        
				label.setText("");
				score.setText("Score: " + correct + " Correct " +
					incorrect + " Incorrect");
				submitButton.setEnabled(false);
			}			
		});        
1 Inside the MathTutor constructor, we create the frame and the remaining components.
2 Action listeners are added to the four operations menu items.
3 An item listener is added to the “Advanced” check box menu item because it requires a different kind of event handler. Note that MathTutor implements both the ActionListener and ItemListener interfaces, allowing it to handle both kinds of events.
4 Next, we add an anonymous ActionListener just for the submit button. It reads the response from the text field, converts it into an integer, and checks against the answer. If the response is correct, it increments the counter for correct answers. Otherwise, it increments the counter for incorrect answers. Finally, it updates the score label and disables the submit button until another operation is chosen from the menu.
        typeMenu.add(advanced); (1)
        operationsMenu.add(add); (2)
        operationsMenu.add(subtract);
        operationsMenu.add(multiply);
        operationsMenu.add(divide);  
        multiply.setEnabled(false); (3)
        divide.setEnabled(false);
        menuBar.add(typeMenu); (4)
        menuBar.add(operationsMenu);        
        frame.setJMenuBar(menuBar);
        // Add components to frame and display GUI (5)
        frame.add(score, BorderLayout.NORTH);
        frame.add(label, BorderLayout.WEST);
        frame.add(field, BorderLayout.EAST);
        frame.add(submitButton, BorderLayout.SOUTH);
		submitButton.setEnabled(false);
        frame.setSize(300, 150);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true); 
    }
1 We add the advanced menu item to the type menu.
2 Then, we add the four kinds of operations to the operations menu.
3 The multiply and divide menu items are disabled because the application starts in basic mode.
4 The menus are then added to the menu bar, and the menu bar is set on the frame.
5 Finally, the labels and text field are added to the frame, which is made visible. Note that these components can be added directly to the frame with the parameters BorderLayout.NORTH, BorderLayout.EAST, and BorderLayout.WEST because JFrame objects use the BorderLayout manager by default.
    public void itemStateChanged(ItemEvent e) {
        if(e.getStateChange() == ItemEvent.SELECTED) {
            add.setEnabled(false); 
            subtract.setEnabled(false);
            multiply.setEnabled(true); 
            divide.setEnabled(true);
        }
        else {
            add.setEnabled(true);
            subtract.setEnabled(true); 
            multiply.setEnabled(false); 
            divide.setEnabled(false);
        }
    }

The itemStateChanged() method enables the multiply and divide menus and disables the add and subtract menus if the advanced check box is selected and does the reverse if it’s been unselected.

    public void actionPerformed(ActionEvent e){
        Object menuItem = e.getSource();	
		if(menuItem == add)
			answer = generator.addPractice(label);
		else if(menuItem == subtract)
			answer = generator.subtractPractice(label);
		else if(menuItem == multiply)
			answer = generator.multiplyPractice(label);
		else if(menuItem == divide)
			answer = generator.dividePractice(label);
		submitButton.setEnabled(true);	
		field.setText("");		
    }

Depending on which menu item fired the event, the actionPerformed() method calls the appropriate method from the ProblemGenerator object to create and then display a math problem on label. The correct answer is stored in answer. Note that this actionPerformed() method doesn’t handle the submit button because it has its own anonymous class handler. We use both styles of listener to keep the code simpler: Handling the submit button separately makes sense because it’s different, but handling the operations menu items together makes sense because they’re similar.

    public static void main(String[] args){       
        new MathTutor();
    }  
}

Last but not least, the main() method creates an instance of MathTutor, initiating GUI construction and display.

15.5. Concurrency: GUIs

Stand-alone Java programs have at least one thread, the main thread. Applications with GUIs create additional threads to manage the GUI behind the scenes.

Although a GUI will create several threads, the most relevant of these is the event dispatch thread (EDT). This thread handles events like button clicks. When you write actionPerformed() methods, remember that the EDT is the one that will actually execute the code inside.

If you’re writing a complex program, the EDT may interact with many other threads, and the synchronization issues discussed in Chapter 14 will become important. However, only the EDT is allowed to change the state of components in a GUI. Using other threads to do so will work some of the time, but it’s not thread-safe and violates the design of Swing.

15.5.1. Worker threads

Thread safety is not the most common multi-threaded GUI problem, however. Unresponsive GUIs can be found on almost every platform, as you’ve no doubt experienced. In Java, unresponsive GUIs usually happen when the programmer uses the event dispatch thread to perform some task that takes too long. Because the EDT is responsible for updating the GUI, the GUI freezes, and the user has to wait.

This problem presents quite a conundrum. On the one hand, the EDT is the only thread allowed to update components. On the other, it has to do its work quickly so that the GUI is responsive. The solution is to spawn worker threads to do the job. When they’re done, they can tell the EDT to update the GUI.

Let’s look at a GUI with two JButton components and two JLabel components. When one button’s pressed, the EDT goes to sleep for 5 seconds before displaying an answer on the first label (in this case, approximately the square root of 2). When the other button’s pressed, it increments a counter and displays the value in the second label.

Program 15.16 GUI that becomes unresponsive when the “Compute” button is pressed.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class UnresponsiveGUI implements ActionListener {
    private JLabel answerLabel = new JLabel("Answer:");
    private JButton computeButton = new JButton("Compute");
    private JLabel countLabel = new JLabel("0");
    private JButton incrementButton = new JButton("Increment");
    private int count = 0;
        
    public UnresponsiveGUI() {
        JFrame frame = new JFrame("Unresponsive GUI");
		frame.setLayout(new GridLayout(4,1));               
        computeButton.addActionListener(this);
		incrementButton.addActionListener(this);
		frame.add(answerLabel);
        frame.add(computeButton);       
        frame.add(countLabel);
        frame.add(incrementButton);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setSize(300,150);
        frame.setVisible(true);		
    }   
    
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == computeButton) {            
            answerLabel.setText("Computing...");
            try {
                Thread.sleep(5000);
            }
			catch(InterruptedException ignore) { }
            answerLabel.setText("Answer: " + Math.sqrt(2.0));
        }
        else {
            count++;
            countLabel.setText("" + count);
        }
    }	
	
    public static void main(String args[]) {
        new UnresponsiveGUI();              
    }
}
unresponsiveGUIFigure
Figure 15.17 GUI generated by Program 15.16.

If you click the “Compute” button, the GUI becomes unresponsive. Specifically, nothing will happen when you click the “Increment” button, but you should still be able to move the frame around the desktop on most systems. Furthermore, some thread in the GUI is registering the clicks you do on the “Increment” button, though events triggered by those clicks aren’t handled until after the EDT wakes up. At that point, the counter will shoot up in value unpredictably.

One solution is to create an anonymous inner class that extends SwingWorker. The SwingWorker class is abstract, but it’s also generic, meaning that it has type parameters (given in angle brackets) which specify what type of objects it interacts with. Generic classes are often containers like LinkedList where the type parameter says what kind of objects will be kept in the list. Chapter 18 covers generics in some depth. The reason we need generics for SwingWorker is so that it can specify what kinds of answers it produces. The first type parameter specifies the type that the worker will return when it completes its work. The second specifies the type that the worker will return periodically in the process of doing work (which can be useful for updating progress bars). Examine the following program which has added a SwingWorker to its actionPerformed() method but is otherwise the same as Program 15.16.

Program 15.17 GUI that uses SwingWorker to avoid becoming unresponsive.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class WorkerGUI implements ActionListener {
    private JLabel answerLabel = new JLabel("Answer:");
    private JButton computeButton = new JButton("Compute");
    private JLabel countLabel = new JLabel("0");
    private JButton incrementButton = new JButton("Increment");
    private int count = 0;  
       
    public WorkerGUI() {
		JFrame frame = new JFrame("Worker GUI");
        frame.setLayout(new GridLayout(4,1));
        computeButton.addActionListener(this);
        incrementButton.addActionListener(this);
        frame.add(answerLabel);     
        frame.add(computeButton);       
        frame.add(countLabel);
        frame.add(incrementButton);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setSize(300,150);
        frame.setVisible(true);		
    }
    
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == computeButton) {
            SwingWorker worker = new SwingWorker<String, Void>() { (1)
                public String doInBackground() { (2)
                    try {
                        Thread.sleep(5000);
                    }
					catch(Exception ignore) {}
                    return "Answer: " + Math.sqrt(2.0); (3)
                }
                
                public void done() { (4)
                    try {
                        answerLabel.setText(get());
                    }
					catch (Exception ignore) {}              
                }
            };            
            worker.execute(); (5)
            answerLabel.setText("Computing...");
        }
        else {
            count++;
            countLabel.setText("" + count);
        }
    }
	
	public static void main(String args[]) {
		new WorkerGUI();
    }
}
1 In this program, the first type parameter for the SwingWorker is String because we’re going to set the text in a JLabel with its result. The second parameter is Void, meaning that we don’t intend to return any values periodically. Most child classes of SwingWorker should override the doInBackground() and done() methods.
2 The doInBackground() method performs the time-consuming work we want done on another thread. In our example, the “work” is just going to sleep, but it will generally be some CPU or I/O intensive process.
3 Afterward, it returns the answer it found.
4 The done() method is called automatically by the EDT after doInBackground() finishes. Note that the get() method automatically returns whatever answer was produced by the doInBackground() method.
5 Once the SwingWorker object has been created, the execute() method starts it running.

This GUI will look identical to the unresponsive version (except for the title), but it will remain responsive.

This syntax is not particularly elegant, but it accomplishes a complex task. It spawns a thread transparently and then notifies the EDT when the thread has its answer ready. Using a SwingWorker isn’t always required, but it’s a useful tool to have in your arsenal if you plan on writing industrial-strength, responsive GUIs.

15.6. Summary

This chapter covers the fundamentals of constructing a GUI to allow users to interact with an application. We show how to add components such as buttons and text boxes and use layout managers to organize their appearance. We show how you can add action listeners and other event handlers so that user interactions like button clicks can trigger useful tasks.

While Java offers a large variety of components and listeners, this introduction is limited to a few of the most commonly used. Once you understand the basics of GUI construction as described in this chapter, it should be easy to understand the extensive Java tutorial at the Oracle Swing Components tutorial site or other reference sources.

15.7. Exercises

Conceptual Problems

  1. Both ActionListener and MouseListener interfaces can be used to process button clicks. Under which circumstances is ActionListener better? Under which is MouseListener better?

  2. Why is the MouseAdapter abstract class useful?

  3. What do you expect will happen if you used setJMenuBar() to set two different menu bars on a single JFrame object?

  4. Describe the situations that the following event listeners are useful for: ActionListener, MouseListener, and ItemListener.

Programming Practice

  1. Write a program that creates a GUI containing two buttons labeled “Start” and “Done.” The GUI frame should be titled “Start and Done.”

  2. Modify Program 15.5 by implementing the mouseExited(), mousePressed(), and mouseReleased() methods. Each method must display a suitable message in the text box when the corresponding event occurs. For example, when the mouse exits button one, the text box should display “Mouse exits One.”

  3. Modify Program 15.8 such that clicking the icon-decorated button plays the sound of a bat squeaking. You will need to add an ActionListener to the button, create an audio clip for the desired sound, and then play this clip when the button is clicked. Consider visiting Freesound.org to find free sound files.

  4. Write a Java program that creates a GUI containing a label with a picture of yourself.

  5. Extend the AnimalSounds class developed in Example 15.9 to include sounds for various animals. There are many publicly available sounds files for use in your program. The Freesound link above is only one source.

  6. In Example 15.9 we stopped both the chirp and the bark sounds because the action listener corresponding to the “Stop Playing” button didn’t know which sound was playing. Modify Program 15.7 so that only the sound that’s playing is stopped. You may need to declare another variable to keep track of which is playing.

  7. Modify Program 15.11 so that it plays a sound associated with an animal when the mouse is clicked over its label. Note that this is an example of a situation where a MouseListener can be used to listen for mouse click events though an ActionListener cannot. As in Exercise 15.7 and Exercise 15.9, you may need to download sounds from the Internet.

  8. Modify the GUI from Section 15.4 to display the problem number the user’s working on. The first problem will be numbered “Problem 1” with subsequent problems 2, 3, and so on. The number should increase each time the user hits the “Submit” button. Find a suitable place on the GUI to display this information. You may need to add a panel to reorganize the GUI.

  9. Remove the actionPerformed() and itemStateChanged() methods from the MathTutor class given in Section 15.4. Move the code from these methods into individual anonymous inner classes added to the add, subtract, multiply, divide, and advanced objects.

    Note that you can now remove ActionListener and ItemListener from the list of implemented interfaces for the MathTutor class. Reorganizing the code this way should have no impact on the functionality of the program. Is doing so a good idea? Why or why not?

  10. In Program 15.3 exchange the first two add() method calls on the panel so that thatButton is added to the panel before thisButton. Explain how the appearance of the GUI is changed.

  11. Modify Program 15.2 by adding a second panel named secondPanel. Create a new button named otherButton with the label “The Other.” Add otherButton to secondPanel. Now add secondPanel to frame and look at the GUI generated. Can you explain why only components from one of the panels is visible?

  12. Remove the line frame.setResizable(false); from Program 15.10. Run the modified program and resize the frame to various sizes. How does the placement of the buttons change?

  13. Modify Program 15.10 by deleting the lines that set the frame size. What does the resulting GUI look like? Now add the following code just before the line frame.setResizable(false);:

    frame.pack();

    When you run the modified program, what’s the impact of using the pack() method?

  14. Create a GUI with a frame that uses GridLayout and has a suitable size. Use a 3 × 2 layout with a horizontal and vertical spacing of 5 pixels each. Use a loop to add eight buttons to your frame, labeled 1 through 8. Observe how the frame expands to include all the buttons even though the GridLayout only specified 6 cells. Depending on the frame size, you might have to resize the window to see all cells and buttons. What happens if you add fewer than 6?

  15. Consider the calculator layout from Program 15.13. Add listeners to all of its buttons so that clicking on the number buttons, the plus and minus buttons, and the “Enter” button will make it perform like a calculator capable of addition and subtraction. Note that you’ll need to add some additional fields to keep track of the current value and whether or not the user has just pressed the plus or minus buttons.

  16. Write a Java program that creates a GUI with a frame and a menu bar containing a single menu. Add a menu item to this menu. Use the add() method, not the setJMenuBar() method, to add the menu bar to the frame. What’s the difference between using the add() method and the setJMenuBar() method to add a menu bar to a JFrame?

Experiments

  1. Recall that Program 15.16 is unresponsive because the event dispatch thread goes to sleep for 5 seconds (5,000 milliseconds). Experiment with this value to determine how much time you can block the EDT before the GUI feels unresponsive.

  2. Program 15.16 is unrealistic because the EDT simply goes to sleep. Normally, a GUI becomes unresponsive because the EDT is performing extensive calculations or doing slow I/O operations. Replace the line that sleeps with a short loop that performs significant calculations. One simple way to spend a lot of computational time is by summing the sines of random numbers, similar to the work done in Example 13.10. How many sines do you need to compute to make the GUI unresponsive for 5 seconds?

  3. Take the computationally expensive loop from Exercise 15.22 and use it to replace the line that sleeps in Program 15.17, the SwingWorker version of the program. Does the program become unresponsive if you run it? If possible, run the program on Windows, macOS, and Linux environments. If it’s unresponsive in some environments but not others, why do you think that might be?

16. Testing and Debugging

I never make stupid mistakes. Only very, very clever ones.

— John Peel

16.1. Fixing bugs

This chapter is about finding, fixing, and preventing bugs in software. Within this book, this chapter is unique in that it’s not based on concrete problems with straightforward solutions. Finding and fixing bugs in software, especially concurrent software, is a problem no one’s solved completely. The first half of this chapter will focus on common bugs and how to fix them. The second half will focus on design techniques for preventing bugs and testing techniques for finding bugs hidden in your code.

16.1.1. Common sequential bugs

We’ll begin with bugs commonly found in sequential programs, some of which have been mentioned in previous chapters as common mistakes. We won’t discuss syntax errors or any other errors which can be caught at compile time. Instead, all bugs discussed in this chapter are run-time bugs.

There’s an infinite number of possible bugs, so they can’t all be listed. Below are a few of the most common categories of bugs to affect beginner (and occasionally veteran) programmers.

Precision Errors

Floating-point numbers have limited precision inside of a computer. Programs that assume infinite precision might break when real results are slightly larger or smaller than expected.

Overflow and Underflow

The cousin of limited precision in floating-point types is the limited range of values that integer types can take on. If a value goes too high, it wraps back around and becomes a negative number. If a value becomes too low, the opposite can happen.

Casting Errors

Casting can have many subtle effects in a program. Sometimes programmers divide two integers and forget that the result is an integer. Incorrectly casting objects can also lead to a ClassCastException.

Loop Errors

Loops are a favorite place for bugs to hide. Three of the most common loop mistakes are:

  • Off-by-one errors: The loop executes one more or one fewer time than expected.

  • Infinite loop: A classic. The loop continues executing until the program runs out of memory or a user stops the program externally.

  • Zero loop: The loop doesn’t execute even once, though the programmer expected it to.

Equivalence Testing Errors

If a programmer uses the == operator to compare two references, it will be true only if the two references point at the same object. Instead, the equals() method should usually be used to compare the attributes of the objects pointed at by the references.

Array Errors

Arrays are often involved with loop errors. Two problems specific to arrays are:

  • Out of bounds: A math mistake or an off-by-one error could lead to an attempt to access an element of an array that comes before index 0 or after the last index.

  • Uninitialized object arrays: Arrays of primitive data types can be created and used immediately; however, arrays of object types are filled with null values until each element is assigned an object, often a new object.

Scope Errors

Some errors can be caused by a programmer’s misunderstanding about the visibility of a variable.

  • Shadowing variables: If a local variable is declared with the same name as a field or class variable, changes within a method will be made to that local variable. This principle is straightforward, but if a programmer doesn’t notice the shadow declaration, the behavior of the code can be very confusing.

  • Reference vs. value: When primitive data is passed as an argument into a method, its value is copied into the method, and the original data is unchanged. When an object is passed as an argument into a method, the reference is unchanged, but methods called on the object can still affect it.

Null Pointer Errors

By this point in your programming experience, you’ve almost certainly experienced a NullPointerException. This last kind of bug is, in many ways, a catch-all category because a null reference is generally not due to a simple typographical error. In the simplest case, a reference simply hasn’t been initialized with some default constructor, but more often there’s a logical error in the design of the code.

16.2. Concepts: Approaches to debugging

When you’ve discovered the existence of a bug, pinning down its cause can be difficult. There are a number of different techniques that are useful for narrowing down the possible problems.

16.2.1. Assertions

A common cause of program errors is an incorrect assumption by a programmer. A programmer can assume that the user will only enter positive numbers, that a library call won’t throw any exceptions, or that a linked list is never empty. Some assumptions are reasonable, but it’s important to make sure that they’re correct.

Using assertions is a way to check some of your assumptions, often those surrounding a method call. In most languages, an assertion tests a condition. If that condition is true, nothing happens, but if it’s false, the program shuts down or an error or exception is thrown. Using assertion statements is particularly important with methods because you want to be sure that both your input and output are in the ranges you expect them to be.

Java supports assertions natively, as we’ll discuss in the next section. However, virtually every language allows you to create your own assertions should they not be present as a language construct.

16.2.2. Print statements

Computer programs execute quickly. Even if they executed a million times slower, no human is sensitive enough to decipher the flow of electrons inside the processor as a program executes. The simple truth is that we have no idea what’s really happening when our programs execute. We believe that we understand our programs well, and the output usually confirms that our programs are doing what we imagine they should be doing.

If the output doesn’t match our expectations, we might not have enough data to understand the problem. As long as there have been programmers, they have been printing out additional debug information to find their errors. This technique can go a step beyond simple assertion statements because you can print out the values of the variables rather than just test to see if they’re in a range.

Once you’ve found the error in your code, it can be tedious to remove all of your debug statements. Some programmers use a special print command that can be turned off using a global variable or compiler option. Others send their output to stderr so that it doesn’t interfere with the legitimate output of the program. Much depends upon the system, the language, and the individual tastes of the programmer.

16.2.3. Step-through execution

With the rise of modern debugging environments, using print statements has lost some of its appeal. Most languages allow the programmer to run his or her program in a special debug mode where it’s possible to execute a single line of code at a time. These tools usually give the option of stepping over method calls or stepping into them on a case by case basis.

As the program executes, the programmer can inspect the values of the variables in the code. This method of debugging is excellent because it allows the programmer to watch the execution of the code at whatever pace he or she desires. Pinpointing problems becomes trivial if you know which variables you need to watch.

Despite the power of this technique, it has critics. Some older programmers look down on these tools because they make new programmers lazier and, in some cases, less careful about writing code correctly in the first place. It should be noted that step-through execution modes are not available for every language or for every system. Some embedded software or operating system programming cannot be debugged on the real hardware in this way. Of course, most of these systems can be run in virtual environments that do allow step-through debugging.

Even when step-through debugging is available, there are difficulties that can limit its effectiveness. If the bug occurs sporadically, perhaps due to race conditions, a programmer might not know where to start looking. Certain data structures such as the list template in C++ might not be easily traversable using the inspection facilities of the debugger. Likewise, the bug or the source of the unexplained behavior could be buried in library code. The debugger doesn’t always have access to library code for stepping through.

16.2.4. Breakpoints

Breakpoints are a feature of step-through debuggers designed to make them easier to use. A user can specify a particular line of code (with some restrictions) as being a place where the debugger should pause execution. Debuggers typically rely on at least one breakpoint in order to skip all the preliminary parts of the code and skip straight to the perceived trouble spot.

Sometimes an error will crop up predictably after many thousands of iterations of a loop or unpredictably due to race conditions or user input. For either of these cases, conditional breakpoints can be used to save the a programmer a great deal of time. Rather than always pausing execution on a given line, a conditional breakpoint will only pause if a certain condition is met.

16.3. Syntax: Java debugging tools

16.3.1. Assertions

As we mentioned before, many languages have assertions as a built-in language construct. In Java, there are two forms this feature takes. The simpler can be done by typing the following.

assert condition;

In this case, condition is a boolean value that’s expected to be true for the program to function properly. The more complicated form of the feature can be used as follows.

assert condition : value;

This form adds a value that can be attached to the assertion to give the user more information about the problem. This value can be any primitive data type, any object type, or a statement that evaluates to one of the two.

If you’ve never used an assert statement before, you might want to test it out by forcing an assertion to fail. You might try the following.

int x = 5;
assert (x < 4) : "x is too large!";

Then, if you compile your program and run it through the JVM, you’ll be shocked when absolutely nothing happens. Actually, some of you with older Java compilers might have heard complaints when you tried to compile this code. If you have a Java 1.3 compiler or earlier, it’ll treat assert like an identifier. Some old Java 1.4 compilers might also give warnings or require special flags to compile. However, if you have an up-to-date compiler, the problem is that the JVM must have assertions enabled at run time. Assertions are intended to be a special debugging tool and ignored otherwise. To turn run program AssertionTest with assertions enabled, type the following.

java -ea AssertionTest

With this option, an exception should be thrown at run time.

Exception in thread "main" java.lang.AssertionError: x is too large!

If you’re using an IDE like Eclipse or IntelliJ, you’ll need to enable assertions on the run configuration for the program, usually by putting -ea in the field for JVM arguments. There are other options allowing you to enable or disable assertions for specific packages or classes.

Now that you know how to use assertions, you need to know when they’re a good idea. The Java Tutorials on the Oracle website suggest five situations where assertions are useful: internal invariants, control-flow invariants, preconditions for methods, postconditions for methods, and class invariants. Internal invariants are those situations when you assume that reaching a certain place in your code, like the else branch of an if statement, will force a variable to have a certain value. For internal invariants, you assert that the variable has the expected value. A control-flow invariant means that you assume that your code will always execute along a certain path. For control-flow invariants, you assert false if the JVM reaches a point in the code you expected it never would. Method preconditions are those conditions you expect to be true about the state of objects or the input to a method before the method is called.

The philosophy of Java is that public methods should not have assertions used to test their preconditions. Instead, illegal input values for a public method should cause exceptions to be thrown, so that improper usage can always be dealt with. In contrast, method postconditions are the states that various variables and objects should have at the end of a method call. Using assertions to check these values is fine, since they reflect an error on the part of whoever wrote the method. Class invariants are conditions about the state of every instance of a class that should be true as long as the class is in a consistent state. Perhaps a method call rearranges the innards of an object, but by the end of the method call, the object should be consistent again. You should use assertions to check class invariants at the end of every method that could make the object violate these invariants.

Wonderful as assertions are, there are times when they shouldn’t be used. The key danger of assertions is that they’re usually turned off. Thus, any statement that’s part of an assertion must not have side-effects that are necessary for the normal operation of the program. For example, imagine you have an object called bacteria that mutates periodically. The mutation returns true if successful and false if there was an unexpected error. You should not test for that failure inside an assert, as follows.

assert bacteria.mutate() : "Mutation failed!";

With assertions disabled, the entire assertion is skipped, and the bacteria object won’t mutate. Instead, your assertion should test only the result of the computation.

boolean success = bacteria.mutate();
assert success : "Mutation failed!";

As stated above, checking for bad input coming into public methods shouldn’t be done with assertions because turning off assertions will remove your error checking.

16.3.2. Print statements

Print statements are one of the most time-honored methods of debugging and remain a quick, dirty, yet effective means of finding errors. Java does not provide any special tools to make print statements easier to use for debugging. Some purists might argue that this kind of debugging which focuses on progressively narrowing down the location of a problem until the bad assumption, logical mistake, or typographical error can be found should be done only with assertions.

Nevertheless, there are a few tips to make print statements a better debugging tool in Java. The first is the use of System.err. By now, you’ve used System.out.print() and System.out.println() so many times that you’re probably tired of them. Any output method that can be used with System.out can also be used with System.err. For example, there’s a System.err.print() and a System.err.println() method. If you simply run a program from the command line and watch the output, you should see no difference between using System.out and System.err. However, if you redirect the output of your program to a file using the > operator, only the System.out code will be sent to the file. Anything printed with System.err will be sent to the screen. Alternatively, you can redirect System.err to a file by using the 2> operator. Using System.err makes it easier to separate legitimate output from error messages, but it also makes it easier to comment out your debug code by doing a find and replace.

A more extensive method for using print statements to debug is by defining your own class for printing. Every method in it can call a corresponding method in System.out or System.err. You can define a boolean value at the class level that determines whether or not methods in your debug printing class print or stay silent. When you want to change from debugging to your production or retail version of the code, you can simply switch this value to false.

A “modernized” method of using print statements is creating a simple GUI instead. In preparing materials for this textbook, we were occasionally frustrated by the fact that multiple threads can interfere with each other while printing on the screen: You can’t always tell which thread is printing which characters. By displaying the output of each thread in separate JTextArea or JLabel components on a simple GUI, you can disentangle the output of each thread.

16.3.3. Step-through debugging in Java

Since the Eclipse and IntelliJ IDEs are so widely used, we’re going to review the step-through debugging features of each environment. Similar tools are available with other IDEs and for most languages.

Eclipse

In Eclipse, you can set set breakpoints either by right clicking or double clicking on the shaded bar immediately to the left of the line you’re interested in or by selecting Toggle Breakpoint from the Run menu. If you try to set a breakpoint on an empty or ineligible line of code, Eclipse will set it on the next legal one.

To debug a program in Eclipse, right click on the file you wish to run in the Package Explorer and select Debug As Java Application. If your program’s already set up to run, you can simply click the Debug button in the toolbar. Whenever you hit a breakpoint, Eclipse will switch to the Debug perspective if it’s not already there.

Once execution is suspended on a breakpoint, you can use the commands Resume, Step Into, Step Over, and Step Return, either from the Run menu or from the toolbar. The Resume command (or F8) allows the program to continue execution, until it hits another breakpoint. The Step Into command (or F5) advances the execution of the program by one statement, moving into a method is there is a method call. The Step Over command (or F6) also advances the execution of the program by one statement, but it skips over method calls. The Step Return (or F7) command advances the execution of the program to the end of the current method and returns, popping the current method off the stack. In the Run menu, there’s also a useful Run to Line command, which will run code until execution reaches the line where your cursor is.

By right-clicking on a breakpoint in Eclipse, you can access its properties. Though properties, you can specify that a breakpoint only halts execution when a specific condition is true or only for a specific thread. Eclipse also provides variable watch and inspection options. Simply by hovering over a variable, its type and value are displayed. You can also inspect an object and traverse its fields. The Variables pane will show the values of local variables, and the Breakpoints pane will show active and inactive breakpoints.

The Debug pane will show a view of the stack for each thread. By moving up and down the stack, the local variables will change depending on which method call you’re currently inside of. By default, the method call displayed will be wherever code most recently executed, but it’s useful to jump back to the method that called the current method (and the method that called that, and so on) to better understand the overall state of the program and why the current method call has the parameters that it does.

IntelliJ

The debugging facilities provided by IntelliJ are similar to those found in Eclipse, and many developers prefer them. You can click in the space to the left of a line of code to set or remove a breakpoint, and you can edit its properties with a right-click.

You can start your program running in debug mode by clicking the Debug button on the toolbar or selecting Debug (Shift+F9) from the Run menu. When execution of your program reaches a breakpoint and pauses, you can use essentially the same tools to advance execution as you would in Eclipse.

The Resume Program command (or F9) will continue execution. The Step Into command (or F7) will advance execution by one line, moving into a method call if there is one. IntelliJ also has a Smart Step Into command (or Shift+F7) that allows you to pick which method call to step into if there’s more than one on the next line. The Step Over command (or F8) advances execution by one line but does not move into a method call. The Step Out command (or Shift+F8) returns from the current method. IntelliJ also has a Run to Cursor command that will try to execute code until reaching the line where your cursor is. All of these commands can be found in the Run menu, and the most common ones appear on toolbars.

In addition, IntelliJ has “force” versions of most of these commands which step over a line of code, step into a method call, or run to your cursor, ignoring breakpoints that might pause execution.

IntelliJ has a Frames pane similar to the Debug pane in Eclipse. It allows the user to select a given thread and jump to different method calls in the stack for that thread. Likewise, IntelliJ has a Variables pane that shows the state of the local variables for the current method call. One of the features that developers like the most about IntelliJ is that it displays the values that variables have on each line where they’re used. In other words, a line of code that adds variables x and y will display their values off to the side of the line of code, updating the display each time the line’s executed.

16.3.4. Examples

Experience is often the difference between a good programmer and a bad programmer. Having seen a bug before means you know to expect it in the future. Don’t be discouraged if it takes hours to find a bug and squash it. Although that time is stressful, spending hours poring over your program makes you better at reading code, a skill just as valuable as writing code. And when you’ve spent hours trying to fix a simple mistake, you’re unlikely to make the same mistake again. To make it easier to spot some of these mistakes, we give a few examples corresponding to the common bugs listed in Section 16.1.

Example 16.1 Precision errors

Floating-point precision can cause subtle errors. Here’s an example of a program attributed to Cleve Moler that gives some estimation of the threshold for floating-point precision. Note that a ≈ 4/3, making b ≈ 1/3, c ≈ 1, and d ≈ 0. Nevertheless, the comparison d == 0.0 in the if statement in this code will evaluate to false.

double a = 4.0 / 3.0;
double b = a - 1;
double c = b + b + b;
double d = c - 1;
System.out.println(d);
if(d == 0.0)
	System.out.println("Success!");

The output for this fragment is -2.220446049250313E-16, and it would be much worse with float variables. Computer scientists who specialize in numerical analysis have tricks for minimizing the amount of floating-point error introduced, but awareness is an easy solution to these kinds of bugs. When testing for specific values of a floating-point number, it’s wise to test for a range rather than a single value. For example, the condition d == 0.0 could be replaced by Math.abs(d) < 0.000001.

Example 16.2 Overflow and underflow

As you know, the int and long types have limited bits for storage. If an arithmetic operation pushes the value of an int variable larger than Integer.MAX_VALUE, the variable will come full circle and become a negative number, usually with a large magnitude. The converse happens when a variable is pushed lower than the smallest value it can hold. These situations are called overflow and underflow, respectively, and Java throws no exceptions when they occur. Programmers who deal with large magnitude values in int or long types get used to underflow and overflow, and when unexpected values are output by their programs, they’re usually quick to pin down the problem variable.

Overflow and underflow can cause more surprising bugs when programmers forget the sharply limited range of values for byte and char types. For example, a curious beginner programmer might want to print out a table of all of the possible values for char. Perhaps the programmer has forgotten the range of values a char can take and is stumped when the following loop does not terminate.

for(char letter = '\0'; letter < 100000; letter++)
    System.out.print(letter);

Each time letter reaches Character.MAX_VALUE which is '\uFFFF' or 65535 as a numerical value, the next increment pushes its value back to 0. These kinds of errors with byte and char values are most common when they’re being used as numbers. Some possibilities are cryptography, low level file operations, and manipulation of multimedia data. The best solution is care and attention. It can help to store the values in variables with more bits such as int or long values, but care must still be taken to ensure that these values are within the appropriate range before storing them back into variables with a smaller number of bits.

For example, color values in many image formats are stored as red, green blue values with a byte used for each of the three colors. In this system, the darkest color, black, is represented as (0,0,0), zero values for each of the three byte values. At the same time, the lightest color, white, is represented conceptually as (255,255,255). In principle, we can perform a simple filter to increase contrast and lightness by doubling all the pixel values. Given red, green, and blue color values stored in three byte variables called red, green, and blue, a naive implementation of this filter might be as follows.

red *= 2;
green *= 2;
blue *= 2;

In Java, this code would not function correctly. The first problem is that, even though image standards are written with color values between 0 and 255, Java byte values are signed. The web standard for the color purple has red, green, and blue values of (128,0,128). Since Java byte values are signed, printing the byte values for each component of purple directly will actually print (-128,0,-128). Multiplying the green value by 2 still gives 0. However, multiplying -128 by 2 as a byte value is -256 which underflows back to 0. Thus, “brightening” purple actually turns it into (0,0,0), black. Properly applying the filter to a byte requires a conversion to the int type, masking out the sign bit, scaling by 2, capping the values at 255, and then casting back into a byte. Despite the complicated description, the code is not too unwieldy.

// Bitwise AND automatically upcasts to int
red = (byte)Math.min(255, 2*(red & 0xFF));
green = (byte)Math.min(255, 2*(green & 0xFF));
blue = (byte)Math.min(255, 2*(blue & 0xFF));
Example 16.3 Casting errors

The previous example about scaling color component values shows one of the dangers of casting. Someone can easily forget that the implicit cast to convert a byte to an int always uses a signed conversion. Likewise, the explicit cast needed to store an int into a byte will cheerfully convert any arbitrarily large int into a byte, even though the final value might not be expected by the programmer.

Many other casting errors commonly crop up. The most classic example might be muddling floating-point and integer types.

int x = 5;
int y = 3;
double value = 2.0*(x/y);

Above, it’s easy for a programmer to forget that the division of x and y is integer division. After all, the 2.0 is right there, causing an implicit cast to double. Of course, this cast happens after the division, and the answer stored into value is 2.0 and not the 3.3333333333333335 that the programmer might have expected.

Newer programmers sometimes forget that an explicit cast from a floating-point type to an integer type always uses truncation, never rounding.

int three = (int)2.99999;

This assignment will always store 2 into three. The Math.round() method or some other additional step is needed to perform rounding.

Casting errors are not limited to primitive data types. Object casting will be discussed at length in Chapter 17. The biggest danger there is an incorrect explicit upcast.

Fruit snack = new ChiliPepper();
Apple apple = (Apple)snack;

In a botanical sense, a chili pepper is indeed a fruit and its parallel Java class is apparently a child of the Fruit class. For some reason, the programmer thought that the only Fruit that would be pointed at by a snack reference would be of type Apple. Instead of a mouth on fire, the programmer gets a ClassCastException. This two line example is so simple that it should never come up in serious programming. A much more common example is an array or linked-list whose contents have a type that’s the parent class of the item you generally expect to be stored. If a large team is working on a body of code with such a list, half of the team might expect the list to contain only Apple objects while the other expected only ChiliPepper objects. The use of generics, discussed in Chapter 18, can reduce the number of casting errors of this kind, but some applications require a list to hold many different types with a common superclass. In those cases, some amount of explicit (and therefore dangerous) casting will usually be necessary when retrieving objects.

Loops give Java much of its expressive power, which includes the power to express incorrect code. Here are examples of a few of the most common loop errors.

Example 16.4 Off-by-one errors

Computer scientists often use zero-based counting. This departure from “normal” practices is just one source of loops that iterate one time more or less than they should. If you want to iterate n times, a good rule of thumb is to start at 0 and to go up to but not including n. Alternatively, if you have a reason not to be zero-based, you can start at 1 and go up to and including n.

for(int i = 1; i < 50; i++)
	System.out.println("Question " + i + ".");

Perhaps you want to make a template for an exam. Instead of being zero-based, you start at 1 because most exams don’t have a Question 0. Unfortunately, you’ve gotten so used to using strictly less than for your ending condition that you forget to change it and only get 49 questions printed out. If your purpose was making an exam, you could catch your mistake and move on. If you’re writing a program that dispenses a quantity of heart medication into a patient’s IV in a hospital, one iteration too few or too many could cause the patient to get too little of the drug to make a difference or too much of the drug to be safe.

Input is another tricky area when it comes to being off by one.

int i = 0;
double sum = 0;
int count = 0;
Scanner scanner = new Scanner(System.in);
while(i >= 0) {
    sum += i;
    System.out.print("Enter an integer (negative to quit): ");
    i = scanner.nextInt();
    count++;
}
System.out.println("Average: " + (sum / count));

This fragment of code appears to be a perfectly innocent loop that finds the average of the numbers entered by a user. The loop uses a sentinel value so that the user simply enters a negative number when all the numbers have been entered. The value of sum is updated before the user enters a value; thus, the harmless 0 from the declaration of i is included but the final negative number entered to leave the loop is not. Unfortunately, the value of count is incremented for every turn of the loop, even the extra one for the negative number. To combat this problem, an if statement could be used inside of the loop or count could simply be initialized to -1. The mistake is a simple one, but it doesn’t jump out at you unless you trace a few executions. What’s most insidious is that the error is going to be small, especially for large sets of input numbers. Catching this kind of bug will be discussed more throughly in the second half of this chapter which deals with testing.

Example 16.5 Infinite loops

Infinite loops come in many different flavors, from the char overflow example earlier to traversing a linked-list which has a cycle in it. Many infinite loops are caused by simple typographical errors. Perhaps the most classic is:

int i = 1;
while(i <= 100);
{
    System.out.println(i);
    i++;
}

It’s usually a beginning programmer who leaves a semicolon at the end of the while header, but even veterans can get overly enthusiastic about semicolons. Often a programmer confronted with such a bug (which causes no output, in this case) will scour the body of the loop without carefully scrutinizing the condition. An extra semicolon at the end of a for loop header will usually cause an error but will usually not cause an infinite loop.

public double average(int[] array) {
    double sum = 0;
    int count = 0;
    for(int i = 0; i < 100; i++) {
        sum += array[i];
        count++;
        if(i == 0)
            i--;
    }
    return sum / count;
}

This example might be too absurd to appear in a textbook, but nearly everyone has written worse code while learning to program. We could suppose that this method is meant to average the values in an array, but for some reason, zero valued entries are not to be counted. The student probably meant to have the following if statement.

        if(array[i] == 0)
            count--;

Those two small changes turn the method into a working but slightly inelegant solution. When debugging remember that index variables in for loops can get changed in the body of the loop and change the expected behavior. Generally, it’s a bad idea to change the value of an index variable anywhere other than the header of a for loop, but there are times when doing so gives the cleanest solution.

Many loop errors are caused by a bad header. Getting the inequality backward or switching increment and decrement will usually make a loop that runs a very long time or not at all. We’ll see the second possibility just a little later.

for(i = 10; i > 0; i++)
	System.out.println(i + "!");

System.out.println("Blast-off!");

In this case, the programmer clearly wanted to count down from 10 to 1, but after so much incrementing, he or she forgot to make i decrement. As a result, the value of i increases for a very long (but not infinite) time, until it overflows.

Example 16.6 Zero loops

On the other end of the spectrum, a bad condition can make a loop execute zero times on for and while loops. For some input, doing so might be intended behavior. In other cases, no input will ever cause the loop to execute.

int i = 0;
double sum = 0;
int count = -1;
Scanner scanner = new Scanner(System.in);
while(i > 0) {
    sum += i;
    System.out.print("Enter an integer (negative to quit): ");
    i = scanner.nextInt();
    count++;
}
System.out.println("Average: " + (sum / count));

We’ve returned to our earlier example of averaging a set of numbers input by the user. This time we’ve initialized count to be -1 to avoid the off-by-one error, but we’ve also changed the inequality of the while loop from greater than or equal to strictly greater. As a consequence, the loop is never entered because 0, the initial value of i, is too small.

public static boolean isPrime(int n) {
    for(int i = 1; i < n; i++) {
        if(n % i == 0)
            return false;
    }
	return true;
}

Here’s a simple method intended to test the number n for primality. Unfortunately, the programmer started the index i at 1 instead of 2. As a consequence, this loop will only run once before finding that any number is divisible by 1. True, this is not a loop that executes zero times, but only once is still just as wrong.

public static boolean isPrime(int n) {
    for(int i = 2; i < n; i++) {
        if(n % i == 0)
            return false;
        else
            return true;
    }
}

This example is similar code trying to solve the same problem. Again, the loop only runs once because the programmer forgot that finding a single case when a number is not evenly divisible by another number does not make it prime: a large range of possible factors must be checked before we can be sure. Many, many beginning programmers make this mistake when asked to solve this problem. Perhaps some insight about the nature of bugs can be gained from this example. By the time a student writes a program of this kind, he or she should have a fair idea of how for loops and if statements work. Likewise, the student will have a fair understanding of the notion of primality. Yet in the process of combining the ideas together, it’s easy to get sloppy and write incorrect code that gives some semblance of being correct.

Example 16.7 Equivalence testing errors

Equivalence is tricky in Java. Very inexperienced programmers confuse the = operator with the == operator, but using the == operator to test for equivalence between two references causes more (and subtler) problems. Comparing two references with the == operator will evaluate to true if and only if the two references point at the exact same object.

String string1 = new String("Test");
String string2 = new String("Test");
if(string1 == string2)
    System.out.println("Identical");
else
    System.out.println("Different");

Because these two String references point to two different String objects, which happen to have identical contents, the == returns false, and the output is Different. With String objects this matter is further confused by a Java optimization called String pooling.

String string1 = "Test";
String string2 = "Test";
if(string1 == string2)
    System.out.println("Identical");
else
    System.out.println("Different");

Because Java keeps a pool of existing String values, only one copy of "Test" is in the pool, and both string1 and string2 point to it. Thus, this second fragment of code prints Identical. Because of String pooling, programmers can write code which can work in some situations and fail in others, if it depends on the == operator.

For String objects as well as every other reference type, it’s almost always the case that the equals() method should be used to test for comparison instead of the == operator. There are a few instances when it’s necessary to know if two references really and truly do refer to the same location in memory, but these instances are a tiny minority.

That said, the equals() method is not bullet-proof. With String objects and most of the rest of the Java API, you can expect good behavior from the equals() method. However, if you create your own class, you’re expected to implement the equals() method. By default, the equals() method inherited from Object only does an equality test using ==.

Properly implementing the equals() method takes care and thought. If your class contains references to other custom classes, you must be certain that they also properly implement their own equals() methods. Likewise, to conform to Java standards, a custom equals() method implies that you have also implemented a custom hashCode() method so that objects that are equivalent with equals() give the same hash value. It seems nit-picky to mention this issue, but many real-world applications depend on the efficient and correct operation of hash tables. Hash tables are data structures used to store and retrieve a key and an associated value. We’ll discuss them briefly in Example 18.4.

16.3.5. Array errors

Any time you have a large collection of data, there are always opportunities for bugs. With catastrophic array bugs, Java usually gives clear exceptions that point you to the line number. Once the exception is thrown, the bug should be obvious. The biggest difficulties arise when some unusual course of events is responsible for the bug cropping up and you have to reconstruct what it is.

Example 16.8 Out of bounds

We have all experienced an ArrayIndexOutOfBoundsException. Either a little carelessness with our indexes or a mistake about the size of the array can lead us to try to access an index that isn’t in the array. In the C language, a negative index is sometimes a legal location, but it never is in Java. Although errors involving negative indexes sometimes occur in code, a more common error is accessing an index slightly larger than the bounds of an array, particularly with a loop.

int[] array = new int[100];
for(int i = 0; i <= 100; i++)
	array[i] = i;

In this example, the last iteration of the loop will access index 100 when array only goes up to index 99.

The causes for going out of bounds can be more subtle. We can imagine an array of linked lists, perhaps for storing English words in a dictionary. If we want to select a list based on the first letter of the word, we could use an array of length 26. Consider the following helper method used to add a new String to the correct list.

public void add(String word) {
	int index = word.toLowerCase().charAt(0) - 'a';
	lists[index].add(word);
}

We can assume that the add() method for a given linked list works properly, but we might have caused other problems already. For one thing, we assumed that word began with either an upper- or lowercase letter, corresponding to our array locations 0 through 25. We’re depending on other code to check the input and throw out String values like "$1" or "-isms", which would map to indexes outside of the 0 to 25 range. Incidentally, we’re also assuming that word has at least one character in it. Even if we expect the input to the method to be error free, the addition of error checking is rarely a bad idea.

Example 16.9 Uninitialized object arrays

Another simple mistake that can occur with arrays is failing to initialize an object array. With a primitive data type like int, creating an array with 1,000 elements automatically allocates enough space to hold those elements and even initializes each one to a default value, 0 in the case of an int. With an object data type, however, each element of the array is a reference to null until it’s initialized.

Hippopotamus[] hippos = new Hippopotamus[15];
hippos[3].feed();

This example causes a NullPointerException. New programmers are often confused by this error because they expect the exception to mention the array or its indexes. For more experienced programmers, this kind of mistake is more of a forehead-slapping oversight than a mind-numbing puzzler that’ll take hours to debug. It’s probably just a matter of instantiating each element in the array before you try to feed those hungry, hungry hippos.

Hippopotamus[] hippos = new Hippopotamus[15];
for(int i = 0; i < hippos.length; i++)
    hippos[i] = new Hippopotamus();
hippos[3].feed();

16.3.6. Scope errors

We don’t have variables in real life, and as a consequence, our intuition about them is sometimes wrong. Which variable you’re accessing at any given time can appear obvious, even if it really isn’t.

Example 16.10 Shadowing variables

Java allows variables in different scopes to be declared with the same identifier. If the scopes are two separate methods, then they’ll never interfere with each other. However, if one scope encloses another, the inner variable will shadow or hide the outer variable.

public class Shadow {
    int darkness = 10;

    public void deepen(int darkness) {
        darkness += darkness;
        if(darkness > 100)
            darkness = 100;
    }

    public int getDarkness() { return darkness; }
}

In this example, the field darkness is being shadowed by the local variable darkness in the deepen() method. It appears that the programmer wanted to increase the field darkness by the amount passed into the parameter darkness and failed to notice that both variables have the same name. As a consequence, the parameter darkness will double itself and then never be used again while the field darkness will never increase. This kind of bug could go uncaught for a long while until a programmer notices that the Shadow object isn’t increasing in darkness no matter how many times it’s told to.

This mistake is also common in constructors, since it’s reasonable to give a certain parameter a name similar to the field it’s about to initialize. Some programmers explicitly prefix all fields with this even though it’s often redundant. Three additions of this will fix the problem in the preceding example.

	public void deepen(int darkness) {
		this.darkness += darkness;
		if(this.darkness > 100)
			this.darkness = 100;
	}

In Java, scope is also defined in terms of classes and their parent classes. A parent class variable can be shadowed by a child class variable of the same name.

public class Bodybuilder {
    public int strength = 8;

    public boolean isStrongEnough(int strengthNeeded) {
        return strength >= strengthNeeded;
    }   

    public void setStrength(int value) { strength = value; }
}
public class BraggingBodybuilder extends Bodybuilder {
    public int strength = 10;

    public void brag() {
        System.out.println("My strength is " + strength + "!");
    }   
}

This example looks like a simple case of inheritance, but whoever wrote the BraggingBodybuilder class mistakenly included the field strength again. As a consequence, any BraggingBodybuilder will always brag that his or her strength is 10, even when code sets his or her strength to other values. When strength is tested, it’ll use the strength field from the superclass Bodybuilder which is set by the setStrength() method. When classes have large numbers of fields, making such a mistake becomes easier.

Dynamic and static binding complicate this scope problem further. The fragment of code below using the class definitions from above highlights these complications.

Bodybuilder builder = new BraggingBodybuilder();
builder.strength = 15; (1)
BraggingBodybuilder bragger = (BraggingBodybuilder)builder;
bragger.brag();
bragger.strength = 20; (2)
bragger.brag();
1 Because fields are statically bound to the class of the object, the strength field for Bodybuilder will be set to 15.
2 However, the strength field for BraggingBodybuilder will be set to 20 here.

Thus, the first call to brag() will print out My strength is 10!, but the second call will print out My strength is 20!. Note that some of the confusion in this example is possible only because the field strength is public. If the field was private and only changed through methods, at least there would no longer be two ways to set the strength field with two different outcomes.

Example 16.11 Reference vs. value

The final category of scope error we’ll talk about occurs when using methods because of confusion between passing by reference and passing by value. Every variable in Java is passed by value. However, when that value is itself a reference, it’s possible to change the values that it references.

public void increaseMagnitude(int number) {
    number *= 10;
}

A novice Java programmer might write a method like the above, expecting the value of number to increase by 10 in the calling code. Some languages like Perl use call by reference as default. Other languages like C++ and C# allow the user to mark certain parameters as call by reference. Programmers comfortable with such languages might be confused about the workings of Java.

On the other hand, becoming used to the pass-by-value style of Java can cause other errors.

public void increaseMagnitude(int[] numbers) {
    numbers[0] *= 10;
}

In this contrasting example, the 0 index element of numbers is increased by a factor of 10. Unlike the previous code, the increase in the value of that element will affect the array passed in by the calling code. The values in the array are shared by the increaseMagnitude() method and the calling code. As you can see, the variable numbers isn’t changing. Reassigning the array reference to a difference array reference would have no affect, but we’re changing an element in the array, not the array reference. The same phenomenon can occur with the fields of objects whose references are passed into a method.

16.3.7. Null pointer errors

Null pointer errors usually raise a NullPointerException in Java. This category of errors is something of a catch-all that could happen for many different reasons, some of which have already been mentioned. A NullPointerException could be raised because the elements of an object array haven’t been initialized. Scope problems could cause a reference to be null if the programmer was mistakenly updating another reference, leaving the reference in question uninitialized.

Though they are common, it’s difficult to give a blanket explanation for why most null pointer errors happen. Usually there’s some fundamental error in program logic. Linked lists and tree structures that rely on null references to mark the end of a list or an empty child node are especially susceptible to these errors.

One significant source of errors is careless usage of method parameters. A programmer might pass in objects that don’t conform to expectations or even null references instead of objects. Well written methods, particularly library calls, should be designed to throw an appropriate exception when this happens. Poorly designed code might blindly use a null reference without checking it first, causing a NullPointerException.

16.4. Concurrency: Parallel bugs

We’ll discuss parallel bugs briefly here because we’ve already spoken in depth about the dangers of parallel programming in Chapter 14. Beyond deadlocks and livelocks, a key difficulty with parallel bugs is that they can make the appearance of sequential bugs nondeterministic and unpredictable.

16.4.1. Race conditions

A race condition describes the situation when the output of a program is dependent on the timing of the execution of two or more threads. Because of the complexity of the JVM and the OS and the fact that many other processes might be running and interacting, it’s usually impossible to determine how two threads will be scheduled. As a consequence, if the output of the program depends on unpredictable timing, the output will also be unpredictable.

In Java, the way that race conditions often impact the program is through some variable shared between multiple threads. When the schedule of threads becomes unpredictable, the changes made to this variable can come out of sequence, and its value becomes unpredictable. Incorrect output means that your program has a bug, but the most frustrating aspect of race conditions is that they’re nondeterministic. Your program could sometimes have the right answer and sometimes not. Your program could always have the wrong answer but not always the same one. The truly insidious issue with race conditions is that they’ll usually cause errors only a tiny percentage of the time. Thus, rigorous testing such as we’ll discuss in the second half of this chapter is necessary to show that a race condition is occurring.

16.4.2. Deadlocks and livelocks

Both deadlocks and livelocks describe situations in which some part of your program will stop making progress because of thread interaction. In the case of deadlock, there will be a circular wait in which thread A is waiting for thread B which is waiting, directly or indirectly, on thread A. In the case of livelock, some repetitive pattern of waiting for a condition that will never be satisfied is still going on, but the threads continue to use CPU time and aren’t simply waiting.

If your program reaches a deadlock state, it won’t terminate. If threads updating a GUI become deadlocked, your window might freeze. Typically, deadlocks are nondeterministic and occur only some of the time. Like all race conditions, they can be difficult to detect and duplicate. In fact, Thread.stop(), Thread.suspend(), and Thread.resume(), three seemingly useful and fundamental methods that were originally part of the Java Thread class, have been deprecated because they are deadlock prone.

16.4.3. Sequential execution

One bug that isn’t even a bug in non-parallel code is sequential execution. This situation arises when, usually due to overuse of synchronization tools, parallel code runs sequentially. Each segment of code, instead of running in parallel, is forced to wait for another to complete. A certain amount of serial execution is necessary to maintain program correctness and avoid race conditions, but Amdahl’s Law gives a rigid, mathematical characterization of how easily speedup can be lost if serial execution makes up large portions of program execution. Because setting up threads and using other concurrency tools has some overheard, a parallel program executing sequentially often runs more slowly than a completely sequential version.

Since programs are usually parallelized for the sake of speedup, it’s useful to time sections of programs to see how well you’ve parallelized them. Sequential execution due to synchronization tools is only one of the many problems that can cause slow execution. The threads might be competing for a limited resource such as an I/O device or might be fighting over a small section of memory, causing cache misses. Tuning applications for maximum performance requires an expert understanding of the concurrency issues within software as well as the underlying OS and hardware characteristics. For now, it’s enough to be aware of the risk of sequential execution and to be as careful as possible when applying locks and other synchronization tools.

16.5. Finding and avoiding bugs

What would you do if you wanted to design software for a system that administers a dose of radiation to a specific location on a patient to help treat them for cancer? Depending on the specification of the problem, you might need to control various voltage sources, read data from sensors, and create a command-line interface or a GUI. With a well-designed specification, you could probably apply your knowledge of loops and control structures to an API and develop a software solution that met requirements, provided that an appropriate hardware platform existed.

But how would you know that it worked? Sure, you could run a series of tests, but how many tests would it take for you to be convinced that it worked perfectly? What if your grade was dependent on it working without a single error? Or your job? Or your life?

You’ve probably already faced the stress of trying to get a program to work as well as possible for the sake of your grade. It’s not such a far cry to imagine your job being on the line if you make a serious mistake as a professional programmer. But what about your life? Perhaps you’ll never put your own life into the hands of code you write, but odds are that you’ve already put your life into the hands of someone else’s code. Software controls airplanes, automobiles, medical equipment, and countless other applications where a bug in the code could result in loss of human life.

Sadly, there have already been cases when such bugs have surfaced with deadly consequences. One of the most famous examples of the dangers of badly written software is the Therac-25. The Therac-25 was a machine designed to deliver therapeutic radiation for medical purposes. Between 1985 and 1987, use of the Therac-25 caused at least six incidents of massive radiation overdoses, leading to at least three deaths.

Like most failures of this magnitude, there was more than a single cause behind the Therac-25 tragedies. For one thing, the machines did give an error code. However, the user manual did not explain the error code, and the technicians were not trained to deal with the errors. Even when patients complained about pain caused by the machines, the technicians and even the manufacturers of the Therac-25 were confident that the machine was operating correctly because neither of the previous models, the Therac-6 and the Therac-20, had suffered any problems. Overconfidence has played a significant role in many of the worst systems failures, including the devastating Chernobyl disaster.

Ignoring the human errors, a number of software errors were also responsible for the Therac-25 overdoses. The overdoses occurred when technicians made incorrect keystrokes giving confusing instructions to the Therac-25 about which mode of operation it should be in. In this situation, the machine would operate with a high-power beam but without the beam spreader that was necessary for its safe operation. The designers ignored the possibility that this series of keystrokes would happen. Also, a race condition was involved in this bug since it depended on one task that set up the equipment and another that received input from the technician. This race condition wasn’t caught during testing because only technicians with long practice could work fast enough to cause the bug. Finally, a counter was incremented for use as a flag variable, but arithmetic overflow occasionally caused this flag to have the wrong value.

In the remaining half of this chapter, we’ll discuss a number of testing methodologies and design strategies to minimize errors in software.

16.6. Concepts: Design, implementation, and testing

Unfortunately, there’s no foolproof way to design software. There are many researchers who work to design new languages and new development tools to limit certain kinds of mistakes, but it’s impossible to design a language as powerful as Python or Java which will also prevent all software bugs. A consequence of the halting problem, a fundamental concept in the theory of computation, is that there’s no way to design a test that will detect all potential infinite loops (or infinite recursion) for all programs.

With careful design, implementation, and testing, most errors can be reduced almost to nothing. In the following subsections, we’ll discuss these three aspects of programming and how you can apply them to writing better programs.

16.6.1. Design

We’ve remarked in the past that good design pays off ten-fold in implementation, and that payoff continues to increase by factors of ten as you move on to testing and eventually deployment.

One of the first design decisions you might have to make is choice of language. Some languages are better designed for certain tasks than others. For example, languages like Ada have been carefully designed to minimize programming mistakes such as mis-matched else blocks. Many functional languages like ML are designed so that memory errors such as a NullPointerException are impossible. Even Java has taken clear steps to avoid some of the errors possible, such as bus errors, in C and similar languages that allow pointer arithmetic. However, many other factors such as portability, compatibility, and speed will affect your language choice.

If you’re working in the software development industry, you might be given a specification from your client or your supervisors. As you design the software needed to meet the specification, you might use UML diagrams to map out the classes and interactions you plan to implement in your program.

There are many questions you might ask yourself as you design your solution. Will your solution be compatible with the system and future changes made to the system? Is it easy to add features to your solution? Does your solution deal gracefully with mistakes in user input or external hardware and software failures? Is your code easy to maintain, particularly by future programmers who were not involved in its initial development? Are the components of the system modular? Can they be worked on, tested, and upgraded independently? Are the components of your system designed well enough to be reused for other applications? Are the elements of your system secure from malicious attacks? Finally, is it easy for the user to work with your software?

Each one of these questions is related to a separate sub-field in software engineering. It might be impossible to address them all completely, but different applications will have different priorities. One method for OO software engineering uses design patterns. The idea behind design patterns is that most classes share some common design principles with a large category of other classes. By naming and recognizing each category, you can apply the same rules to designing new classes from a category you’re already familiar with. Each category is called a design pattern. Java uses design patterns extensively in its API. Describing design patterns in greater depth is beyond the scope of this book, but you might want to consult the Gang of Four’s popular book Design Patterns.

Another important idea in design is design by contract. Although this is also a rich, complex area of software engineering, the idea can be applied to methods in a straightforward way. For each method, you have a formal explanation of what its input should be, what its output should be, and what else can be changed in the process. For some languages and some segments of code, it’s possible to prove that a given method does exactly what it’s supposed to do. Nevertheless, Donald Knuth, a giant in computer science, is famous for having said, "Beware of bugs in the above code; I have only proved it correct, not tried it."

16.6.2. Implementation

Once you have gotten your design to the implementation phase, there are a number of other techniques you can use to minimize errors. One interesting technique is pair programming, in which two programmers sit at a single computer and work together. Ideally, the programmer who is currently typing, often called the driver, is thinking about the immediate problems posed by the next few lines of code while the other programmer, often called the navigator, is thinking about the larger context of the program and watching for errors. Two sets of eyes are always beneficial when looking at something as detailed and confusing as a computer program.

In keeping with the theme of having more than one set of eyes looking at a program, it’s also useful to have the individuals who test the software be independent from those who develop it. By keeping the testers separate, they’re not infected by the assumptions and biases that the developers made while writing the software. Some communication between the two groups is necessary, but there’s value in black-box testing, which we’ll explain in the next subsection.

Another piece of general advice is to rely on standard libraries as much as possible. Reinventing your own libraries is partly a waste of time and partly dangerous because your own libraries haven’t undergone as much testing as the standard ones. Likewise, it makes your code less portable. Some expert developers might need to write special libraries for speed or memory efficiency, but they’re the exception, not the rule.

There are a number of Java specific implementation guidelines. People have written entire books about good software engineering in Java, and so we’ll only give a few obvious pointers.

Although it’s tempting to do so when working under time pressure, never write empty exception handlers. Doing so swallows exceptions blindly, giving the user no information about the errors in his or her program. By the same token, always make your exception handlers as narrow as possible. Simply putting catch(Exception e) at the end of any try-block has one of two possible outcomes: In one case your handler is vague and the user is informed that a general error of some kind has occurred. In the other your handler is more specific than it has a right to be. You might have assumed that a file I/O error was likely to occur and always report that failure. Instead, an ArrayOutOfBoundsException could happen and be mistakenly reported as a file I/O problem.

You should test the input to any public methods you write and throw a pre-determined exception if the input is invalid. Never use assertions to test input to public methods. In fact, you should never depend on assertions to catch errors since they must be turned on in the JVM to have effect. Assertions are great for debugging code before it’s released but have little or no value in the field.

16.6.3. Testing

Once you’ve designed and implemented your program (or ideally throughout the process of implementation), you should test it to see if it behaves as expected and required. The most common form of software testing done by students is a form of smoke test. A smoke test is a basic test of functionality. Such a test should simply run through the major features of a program and verify that they seem to work under ordinary circumstances. Often a student will barely finish the program before the deadline and be unable to perform anything but the most basic tests.

Smoke tests are useful because it’s pointless to test the finer details of a system that’s clearly broken, but the software engineering industry uses many other kinds of testing to ensure that a given piece of software meets its specification. We’ll briefly cover three broad areas of testing: black-box testing, white-box testing, and regression testing.

Black-box testing

Black-box testing assumes that the tester knows nothing about the internal mechanisms of the software he or she is testing. The software is viewed as a “black box” with inputs and outputs but otherwise unknown internals. The tester chooses some subset of the possible inputs and tests to see if the output matches the specification.

For simple programs with very little input, it might be possible to test all possible input values, but doing so is impractical for most programs. A short list of techniques for determining the appropriate set of input values for black-box testing follows.

Equivalence Partitioning

The idea behind equivalence partitioning is that large ranges of data might be functionally equivalent from the point of view of causing errors. If a tester can run a test for one element from a range of data, then the entire range can be tested quickly. To perform this kind of testing, the tester must partition data into ranges that function differently. The partition created is usually not really a partition in the mathematical sense as the sub-domains are overlapping. For this reason equivalence partitioning is also referred to as subdomain testing.

For example, a program controlling the temperature of the water in an aquarium might have legal input ranges between 32 °F and 212 °F. However, if the program warms the water when it’s below 75 °F and cools it when it’s above 90 °F, then values below 0, values from 0 to 74, values from 75 to 90, values from 91 to 212, and values above 212 all constitute different partitions.

Boundary Value Analysis

Once inputs have been partitioned into equivalent ranges, testers can focus on values which are near the boundaries of those ranges. For example, an input containing a person’s age might be allowed to range between 0 and 150. The values -1, 0, 1, 149, 150, and 151 are good candidates for input from the perspective of boundary value analysis. As with equivalence partitioning, boundary value analysis is useful not only for the boundaries between valid and invalid data but also for the boundaries between any input ranges with different program behavior such as the boundaries separating the five ranges of values for the aquarium thermostat program described above.

All-Pairs Testing

Most software bugs are triggered by a single piece of input. Some harder to discover bugs require two separate pieces of input to have specific values at the same time before they manifest. With each increase in the number of different inputs that must each have specific values at the same time to cause a bug, the bug becomes increasingly difficult to detect but also increasingly unlikely to exist. It might be possible to test all possible values for a given input but impossible to test all possible values for all inputs at the same time. All-pairs testing is a compromise between these two extremes that tests all possible pairs of inputs.

Fuzz Testing

The concept behind fuzz testing is to use large amounts of invalid, unlikely, or random data as input to a program. Although this kind of testing is used only to test the reliability and robustness of a program receiving unexpected input, it has a number of advantages. One significant advantage of fuzz testing is that it’s quick and easy to design test cases. Another is that it makes no assumptions about the program behavior, catching errors that might never occur to a human being. If fuzz testing is automated, it can also be used for stress testing, in which the program’s ability to process a large amount of data quickly or while remaining responsive is tested.

White-box testing

The philosophy of white-box testing is the opposite of black-box testing. When using white-box testing techniques, the tester has access to program internals. The tester should employ techniques to test every possible path that execution can take through the code. Traversing a particular path of execution through a program is called exercising that path.

In order to exercise every possible path, it’s necessary to force each conditional statement to be true on some path and false on another. Some combinations of true and false might be impossible, but ignoring this fact, a program with n independent conditionals would require 2n runs to test them all. Because of the large number of possible execution paths, white-box testing generally tries to maximize coverage over metrics that are not quite so demanding.

Method coverage is the percentage of methods that are called by test cases at least once. Ideally, this number is 100%. Statement coverage is the percentage of statements that are executed by test cases. Again, this number should be as close to 100% as possible. Branch coverage is the percentage of conditionals that have been executed on both their true and false branches. Getting total coverage here is difficult, but good testing can come close.

As with black-box testing, equivalence partitioning and boundary value methods can be used to reduce the total number of test cases. Also, it’s important to test those parts of your programs reached only in error conditions in addition to normal operation.

Regression testing

Regression testing is a form of testing that’s not often necessary for student code, which is usually focused on small projects. The motivating idea behind regression testing is that, in the act of fixing a bug or adding a feature, existing code can be broken. Thus, even after a system has been thoroughly tested, small changes or additions require the entire system to be retested. As the size of a program grows, the chance of unintended consequences increases, along with the value of performing regression testing.

Regression testing can incorporate both black- and white-box testing. Doing regression testing could simply mean running all the existing tests over after every major change. At the very minimum, each time a test uncovers a bug, that test should be added to the test suite used after each build of the program. The use of regression testing also implies that regular testing is being done on your code. Regular testing gives developers the opportunity to track changes in other aspects of their program such as memory usage, running time, responsiveness, and other non-functional issues.

16.7. Syntax: Java testing tools

One open-source tool for testing Java is called JUnit. There are other testing tools for Java, and there’s a wide array of tools for testing software in virtually any language. We cover JUnit here because it’s widely accepted as a standard Java testing tool and because it’s open source.

16.7.1. JUnit testing

JUnit testing is used for unit testing Java. Unit testing is the process of testing separate software components that will eventually work together. By testing them individually, debugging can be done before interactions between different components make it more difficult to find the underlying bugs. After unit testing comes integration testing to test how the components work together. Finally, system testing is the testing of the complete, integrated system against its specifications.

Annotations

Our coverage of JUnit testing is based on JUnit 5. This version of JUnit has simple syntax for creating JUnit tests compared to JUnit 3 and earlier, but it also relies on annotations. An annotation is additional information written into Java code that affects how the compiler or run-time system treats the code. They are like comments, but they can affect code execution or compilation, usually indirectly. Applying an annotation to a method is called decorating. A class, a method, a variable, a package, or even an individual method parameter can be decorated.

There are several annotations built into Java. Here, we consider three: @Deprecated, @Override, and @SuppressWarnings. If a method is decorated with @Deprecated, it’s deprecated and included only for backward compatibility. The compiler will give a warning if you call deprecated code such as the following.

@Deprecated
public void oldMethod() {
    ...
}

Many methods in the extensive Java API are deprecated, like Thread.suspend(), due to its inherent deadlock risk. As of Java 5 when annotations were introduced, existing deprecated methods were decorated with @Deprecated. Before that, the only way to know that a method was deprecated was by reading the documentation. The @Override annotation marks a method that’s overriding a parent class method, causing a compiler error if the method isn’t correctly overriding some parent class method. The @SuppressWarnings annotation allows certain warning messages to be suppressed, like using deprecated code if you really have to.

Basic JUnit syntax

First of all, JUnit is not a part of the standard Java API. To use it, you should download the latest jar file from the JUnit site and add the path to that jar file to your class path. Because JUnit is so universal, Eclipse, IntelliJ, and many other IDEs provide a way to add the JUnit library to a project without the need to download the jar file separately.

To access the JUnit facilities in your code, you need the following import. Note that this import is different from JUnit 4 and earlier versions which used classes from the org.junit package.

import org.junit.jupiter.api.*;

Then, you need to set up a testing class just like you would any other class. The key difference is that each method in the testing class is designed to test some functionality of a code component. For example, let’s imagine that we want to test functionality within the Java Math class such as the ceil(), pow(), and sin() methods.

To do so, we create a class called MathTest with three methods inside of it called ceilTest(), powTest(), and sinTest(). Note that it’s a common JUnit convention to end the names of the testing methods with “Test.” We’ll use each method to test the functionality of the three methods that, respectively, have matching names. Although ending with “Test” is a convention, there’s no requirement to name these methods any particular way. Tests in JUnit don’t have to test single method calls. They could test any functional aspect of an object or class. Nevertheless, for documentation reasons it’s wise to give the test methods names that reflect what’s being tested.

Where do annotations come in? The header for the ceilTest() method should be as follows.

@Test
public void ceilTest()

The only thing necessary to use a method for a JUnit test is to annotate it with @Test. It’s also necessary to make any function used for testing public with a void return type and no parameters. Otherwise, the JUnit framework will crash when you try to run the tests. Each method with a @Test annotation is run once by JUnit, but JUnit can’t supply any arguments to them. They should be self-contained tests without any outside input.

The exception to this rule is that you can perform some set up for the tests and then some clean up afterward. Any method decorated with @BeforeEach will be run before every test, and any method decorated with @AfterEach will be run after every test. If you have some set up or clean up that’s expensive to run, you can use the annotations @BeforeAll or @AfterAll to decorate a static method that’s run once before or after all the tests.

So far we have talked about the major aspects of writing a JUnit test class except for the test itself. How does the JUnit test report a success or a failure to the tester? As you would expect in Java, we use the exception handling mechanism to indicate failures. If the test method returns normally, the test is considered a success. If an unhandled exception or error is thrown by the method, the test is considered a failure. One of the most common ways of implementing this is by using a form of assertions.

Of course, you could simply add an assert into the test code, then enable assertions while running the test, but this approach means that your tests could all incorrectly pass if you forget to enable assertions. Instead, use the following import.

import static org.junit.jupiter.api.Assertions.*;

With this static import, you’ll have access to many static methods that provide useful assertion functionality. The simplest of these is assertTrue(), which is essentially equivalent to an assert without requiring assertions to be enabled. For example, we could code the body of the ceilTest() method as follows.

@Test
public void ceilTest() {
    assertTrue(4 == Math.ceil(3.1));
}

Another useful method is assertEquals() (and its close cousin assertArrayEquals()) which takes two parameters and throws an AssertionError if the two aren’t equal. There are overloaded versions of this method for other primitive types and Object. Note that the preferred assertEquals() method for the double type takes three parameters, including a delta threshold in case the values don’t match exactly.

Example 16.12 JUnit math testing

Using these methods, we can finally write a complete (though simple) implementation of MathTest.java.

Program 16.1 Simple testing suite.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class MathTest {  
	private static double sqrt2;
	private static final double THRESHOLD = 0.000001;

	@BeforeAll
	public static void setUp() { sqrt2 = Math.sqrt(2);}

	@Test
	public void ceilTest() { 
		assertTrue(4 == Math.ceil(3.1));
	}

	@Test
	public void powTest() { 
		assertEquals(2, Math.pow(sqrt2, 2), THRESHOLD);
	}

	@Test
	public void sinTest() { 
		assertEquals(sqrt2/2.0, Math.sin(Math.PI / 4.0), THRESHOLD);
	} 
}

Note that the setUp() method is extremely trivial here, and no clean up is needed.

JUnit has many other powerful features that allow you to run suites of tests or repeated tests with specific parameterized values, but we’re only going to introduce one more feature here. In an ideal world, you develop tests as you develop code. In fact, a popular software development methodology is called test-driven development (TDD). The central idea behind TDD is writing your tests before you write your code. Doing so provides clarity about what you want your code to do, a way to know that you’ve written your code correctly, a way to document your code, and a suite of tests that grows along with your program.

Whether doing TDD or not, you might have completed a test for a specific feature before you’ve finished implementing it. Or perhaps a feature in your program is broken at the moment, but you want to continue running tests on the rest of the features. In these cases and others, it’s useful to turn off a particular test temporarily. To do this, you add the annotation @Disabled before the @Test annotation. In parentheses after the @Disabled annotation, you should ideally put in parentheses a String giving the reason why the test is being disabled. There are even annotations that allow you to run tests or disable them for particular platforms.

Running JUnit

Once you’ve created your JUnit test classes, you’ll want to run them. There are tools built into IDEs like Eclipse to make running easier, but the command line is always an option. As we said before, you need to include the JUnit jar file in your classpath. You can either do this permanently, by adding it to a CLASSPATH environment variable in a way dependent on your OS, or for a particular run of a Java tool. Assuming that you haven’t added the jar file to your classpath permanently, let’s say that you’re using the JUnit Platform 1.5.1 that’s included with JUnit 5.5.1 from a jar file called junit-platform-console-standalone-1.5.1.jar that can be found in C:\Java\JUnit\. If MathTest.java is in the current directory, you would type the following.

javac -classpath .;C:\Java\JUnit\junit-platform-console-standalone-1.5.1.jar MathTest.java

To actually run the tests, you need to tell the platform where to look for tests to run. Thus, you run the platform from its jar file, specifying that the class path is the current directory (.), and then scan that directory for tests. Note that it’s unnecessary to mention MathTest at this point since all tests in the specified class path will be run.

java -jar C:\Java\JUnit\junit-platform-console-standalone-1.5.1.jar --class-path . --scan-class-path

The output should include something like the following, showing that all three tests passed.

â•·
├─ JUnit Jupiter ✔
│  └─ MathTest ✔
│     ├─ ceilTest() ✔
│     ├─ powTest() ✔
│     └─ sinTest() ✔
└─ JUnit Vintage ✔

For the command line, this output is impressive, yet the commands listed above to run JUnit are complex. These commands have many additional options, allowing you to include or exclude additional paths, directories, and file names. While it’s possible to run JUnit commands from the command line, the process is smoother through an IDE.

16.8. Concurrency: Testing tools

In this section, we describe some tools that exist specifically to help catch those bugs that crop up as a direct result of concurrency. This section is short, and that shortness reflects the fewness of good tools available. The design of concurrent debugging and testing tools is still an open research topic. The heart of the problem is that the nondeterminism present in concurrency makes bugs difficult to pin down. You could run a JUnit test 1,000,000 times and never see a peculiar race condition. From a brute force perspective, we could try to test all possible interleavings of thread execution, but this approach is not practical for large programs because the number of interleavings grows exponentially. Nevertheless, some research has focused on attacking the problem from this direction.

16.8.1. IBM ConTest

One tool that used this idea was ConTest from IBM. Normal JVM operation makes some interleavings more likely than others. If the correct output is very likely and the incorrect is very unlikely, it’s easy for a tester to believe that the program works correctly. ConTest was a tool that instrumented class files after they’d been compiled by Java. When it instrumented these files, it added extra method calls into concurrent code designed to introduce some randomness into the system. By introducing sleep() and yield() methods in random places, the JVM could be forced into producing interleavings that would otherwise be unusual. The designers of ConTest used heuristics so that ConTest added this randomness in “smart” locations designed to maximize unusual interleavings and catch bugs.

ConTest was not a panacea. Although it revealed bugs that were rare, it still had to be combined with strong testing methodologies so that those bugs could be caught when they appeared. Another difficulty with using ConTest was that it couldn’t tell you where the problem happened or when it was likely to happen under normal circumstances. You were still dependent on your test design to reveal the source of the problem. ConTest also didn’t guarantee every possible ordering. Very rare bugs might not have manifested even after thousands of runs with ConTest instrumented code. Although ConTest was one of the most promising tools taking this approach, it is no longer available.

16.8.2. Devexperts tools

The open-source Lin-check framework has some similarities to ConTest. It was designed by Devexperts specifically to test data structures such as those we will discuss in Chapter 18. Like ConTest, Lin-check executes tests in parallel many times in an effort to find a concurrent interleaving that causes an error condition.

The Dl-check tool was also designed by Devexperts. Its goal is to find deadlocks that could happen in concurrent programs.

16.8.3. ConcJUnit and ConcurrentUnit

We hope we’ve convinced you of the value of using JUnit testing to unit test your programs. Of course, JUnit has limitations, especially when testing concurrent programs. JUnit uses exceptions to report failed test cases, but JUnit only reports exceptions from the main thread, not from any child threads that might be spawned. ConcJUnit allows exceptions thrown by child threads to be reported and also forces all child threads to join with the main thread.

In this way, it will be clear if any errors happened while a child thread was being executed, either causing an exception to be thrown or causing a child thread to fail to rejoin the main thread. ConcJUnit is intended as a drop-in replacement for JUnit, but only for JUnit 3 and 4. ConcJUnit is no longer under active development and is unlikely to have a version compatible with JUnit 5. ConcJUnit is part of a larger suite of open-source tools called Concutest maintained at the Concutest site.

ConcurrentUnit is another testing framework with annotations similar to those found in JUnit. Although it’s not intended as a replacement for JUnit, it has facilities like ConcJUnit that allow testers to discover when an exception occurs on child threads.

16.8.4. SpotBugs

SpotBugs (previously called FindBugs), is a static analysis tool that examines programs for a long list of errors. SpotBugs looks for patterns that match known errors. Most of the errors that SpotBugs looks for have nothing to do with concurrency, but a whole section of its bug list is devoted to multithreaded correctness. SpotBugs can help you find incorrect usage of synchronization tools as well as unsafe concurrent library usage.

16.8.5. Intel® tools

There are industry tools for debugging and optimizing threaded programs outside of Java. Intel® produces software such as the Intel® Inspector to find concurrent errors as well as the Intel® Vtune™ Amplifier to help tune threaded programs. These products from Intel®, like many concurrency tools, are focused on C/C++ and Fortran platforms. Historically, concurrency has been centered in the high performance and scientific computing markets. Java, in contrast, has been perceived as a slow language, better suited to desktop applications. As the role of concurrency continues to evolve, so will the tools to help programmers.

16.9. Examples: Testing a class

The larger the system, the more critical testing becomes. We don’t have the space to explain a complex testing example, but we can provide another example of JUnit testing.

Example 16.13 A broken class

Using an example from physics, we can create a PointCharge class that has a certain charge and a specific location in 3D space. We’re also going to introduce some errors into the class. Because the class is so simple, the errors should be obvious. Nevertheless, we’ve picked reasonable errors that might come up in real development.

Program 16.2 Physics class with errors.
public class PointCharge {  
    private double charge;  // C
	private double x;		// m
	private double y;		// m
	private double z;		// m
    public static final double K = 8.9875517873681764e9; // N m^2 C^-2  
      
    public PointCharge(double charge, double x, double y, double z) { (1)
        this.charge = charge;    
        this.x = x;
        this.y = y;
        this.z = y;
    }
  
    public double getCharge() { return charge; }
  
    public double distance(PointCharge p) { (2)
        return distance(p.x, p.y, p.z);
    } 
  
    private double distance(double x, double y, double z) { (3)
        double deltaX = this.x - x;
        double deltaY = this.y - y;
        double deltaZ = this.z - z;    
        return Math.sqrt(deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ);      
    }
  
    public double scalarForce(PointCharge p) { (4)
        double r = distance(p);
        return K*charge*p.charge/r*r;
    }
  
    public double fieldMagnitude(double x, double y, double z) { (5)
        double r = distance(x, y, z);
        return charge/(r*r);
    }
}
1 The PointCharge class has a straightforward constructor followed by an accessor.
2 Then, it has a method to determine distance to another PointCharge.
3 This method in turn relies on a private helper method that can compute distance from an arbitrary x, y, and z location.
4 Some of the harder work done by the class can be found in a method to determine the scalar force between two charges.
5 Another method finds the magnitude of the electric field due to the charge at some location.

Recall from physics that the force F between two charges q1 and q2 is given by the following equation.

force

In this equation, ke is the proportionality constant 8.9875517873681764 × 109 N⋅m2⋅C-2 and r is the distance between the charges. Likewise, the electric field E at a given location due to a charge q is given below.

field
Example 16.14 Testing the distance methods

Let’s come up with a test for the distance() methods first. We’re going to need some other PointCharge objects. Let’s make four altogether: one at the origin and three one meter along each positive axis. We can create these charges in a set up method. While we’re at it, we’ll give them a variety of positive and negative charges.

@BeforeEach
public void setUp() {
	charge1 = new PointCharge(1, 0, 0, 0);
	charge2 = new PointCharge(2, 1, 0, 0);
	charge3 = new PointCharge(-1, 0, 1, 0);
	charge4 = new PointCharge(0, 0, 0, 1);
}

To test the distance() method thoroughly, we’ll check the distance from charge1 to all the other charges as well as charge2 to charge3.

@Test
public void distance() {
    assertEquals(1.0, charge1.distance(charge2), 0.000001);
    assertEquals(1.0, charge1.distance(charge3), 0.000001);
    assertEquals(1.0, charge1.distance(charge4), 0.000001);
    assertEquals(Math.sqrt(2.0), charge2.distance(charge3), 0.000001);
}

The distances between charge1 and the other three should be 1, and the distance between charge2 and charge3 should be around the square root of two. Yet when we run this test with JUnit, the test fails with the following error.

java.lang.AssertionError: expected:<1.0> but was:<1.4142135623730951>

This error happens on the second assertion in the method. But why? If we comb through the distance() methods in PointCharge, they all look correct. The problem must be deeper. PointCharge doesn’t have accessor methods for its location, so we can’t test those. Checking the constructor, we find the culprit: this.z = y;, a simple cut and paste error.

With the distance() methods working, we can run a similar test for scalarForce() generated by plugging in appropriate values into the equation for F.

@Test
public void scalarForce() {
	assertEquals(2*PointCharge.K, charge1.scalarForce(charge2), 0.000001);
	assertEquals(-PointCharge.K, charge1.scalarForce(charge3), 0.000001);
	assertEquals(0.0, charge1.scalarForce(charge4), 0.000001);
	assertEquals(-PointCharge.K, charge2.scalarForce(charge3), 0.000001);
}

When we run this test with JUnit, the last assertion fails. We get the following output.

java.lang.AssertionError: expected:<-8.987551787368176E9> but was:<-1.797510357473635E10>

A close inspection reveals that the actual value is about twice the expected value. Where does this extra factor of 2 come from? Scanning the code for scalarForce(), we find return K*charge*p.charge/r*r;

We forgot parentheses and messed up our equation. What we really wanted was return K*charge*p.charge/(r*r);

The most striking thing about this example is that three test cases passed! Perhaps that means that we were choosing values that were too simple, but it also illustrates the importance of thorough testing.

Example 16.15 Testing the field magnitude method

Finally, let’s test the value of the fieldMagnitude() method. For simplicity, we’ll test the field at the locations of charge1, charge3, and charge4 with respect to charge2.

This time the first assertion fails. We get the following output.

java.lang.AssertionError: expected:<1.797510357473635E10> but was:<2.0>

2.0 seems like a very strange result when we were expecting a value with an order of magnitude 10 times larger. Perhaps a constant was omitted? Yes, our version of fieldMagnitude() left off a factor of K. Once we fix that, our code finally passes all three tests. Why didn’t we fail the assertions after the first one? Because of the exception handling mechanism, each JUnit test method stops once a failure has happened.

Here is the fully corrected version of PointCharge renamed FixedPointCharge.

Program 16.3 Corrected version of PointCharge.
public class FixedPointCharge {  
	private double charge;  // C
	private double x;		// m
	private double y;		// m
	private double z;		// m
	public static final double K = 8.9875517873681764e9; // N m^2 C^-2  

	public FixedPointCharge(double charge, double x, double y, double z) {
		this.charge = charge;    
		this.x = x;
		this.y = y;
		this.z = z;
	}

	public double getCharge() { return charge; }

	public double distance(FixedPointCharge p) {
		return distance(p.x, p.y, p.z);
	} 

	private double distance(double x, double y, double z) {
		double deltaX = this.x - x;
		double deltaY = this.y - y;
		double deltaZ = this.z - z;    
		return Math.sqrt(deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ);      
	}

	public double scalarForce(FixedPointCharge p) {
		double r = distance(p);
		return K*charge*p.charge/(r*r);
	}

	public double fieldMagnitude(double x, double y, double z) {
		double r = distance(x, y, z);
		return K*charge/(r*r);
	}
}

And, for easy readability, here’s the full JUnit test class TestPointCharge. Note that you will have to change the name PointCharge to FixedPointCharge if you want to test the corrected class.

Program 16.4 Class for testing PointCharge.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class TestPointCharge {  
	private PointCharge charge1;
	private PointCharge charge2;
	private PointCharge charge3;
	private PointCharge charge4;

	@BeforeEach
	public void setUp() {
		charge1 = new PointCharge(1, 0, 0, 0);    
		charge2 = new PointCharge(2, 1, 0, 0);
		charge3 = new PointCharge(-1, 0, 1, 0);
		charge4 = new PointCharge(0, 0, 0, 1);
	}

	@Test
	public void distanceTest() {
		assertEquals(1.0, charge1.distance(charge2), 0.000001);
		assertEquals(1.0, charge1.distance(charge3), 0.000001);
		assertEquals(1.0, charge1.distance(charge4), 0.000001);
		assertEquals(Math.sqrt(2.0), charge2.distance(charge3), 0.000001);
	} 

	@Test
	public void scalarForceTest() {
		assertEquals(2*PointCharge.K, charge1.scalarForce(charge2), 0.000001);
		assertEquals(-PointCharge.K, charge1.scalarForce(charge3), 0.000001);
		assertEquals(0.0, charge1.scalarForce(charge4), 0.000001);
		assertEquals((double)-PointCharge.K, (double)charge2.scalarForce(charge3), 0.000001);
	}   

	@Test
	public void fieldMagnitudeTest() {
		assertEquals(2*PointCharge.K, charge2.fieldMagnitude(0, 0, 0), 0.000001);
		assertEquals(PointCharge.K, charge2.fieldMagnitude(0, 1, 0), 0.000001);
		assertEquals(PointCharge.K, charge2.fieldMagnitude(0, 0, 1), 0.000001);    
	}   
}

16.10. Exercises

Conceptual Problems

  1. What’s the purpose of the assert keyword in Java? What steps must be taken for it to be active?

  2. What’s the value of j after the following statements are executed?

    int j = 1;
    int i;
    for(i = 0; i < 10; i++);
    	j += i;

    Assuming the programmer made an error, what category of programming error does it fall under?

  3. The following loop is intended to print out all possible byte values. What’s the conceptual error made in the following loop? How many times will it execute?

    for(byte value = 0; value < 256; ++value)
    	System.out.println("Byte: " + value);
  4. What are all the possible run-time errors that could occur in this method that reverses a section of an array?

    public void reverse(Object[] array, int start, int end) {
        Object temp;
        end--;  // Up to but not including end
        while(start < end) {
            temp = array[start];
            array[start] = array[end];
            array[end] = temp;
            start++;
            end--;
        }
    }

    What checks could be added to catch these errors?

  5. Recall the idea of a stack mentioned in Section 9.1. Consider the following definition of a stack designed to hold int values.

    public class IntegerStack {
        private static Node {
            public int data;
            public Node next;
        }
    
        private Node head = null;
    
        public void push(int value) {
            Node temp = new Node();
            temp.data = value;
            temp.next = head;
            head = temp;
        }
    
        public void pop() { head = head.next; }
        public int top() { return head.data; }
        public boolean isEmpty() { return head == null; }
    }

    What exceptions could be thrown when using this class? Where could they be thrown?

  6. Imagine that you have a simple linked list such as the ones described in Section 18.2.2. What if there’s a loop in the list such that the last element in the list points to an earlier element in the list? For this reason, a simple traversal of the list will go on forever. How could you detect such a problem during program execution? Note that this question sometimes comes up in job interviews.

  7. What’s the difference between black-box testing and white-box testing? What kinds of bugs are more likely to be caught by black-box testing? By white-box testing?

  8. The Microsoft Zune was a portable media player in competition with the Apple iPod. The first generation Zune 30 received negative publicity because many of them froze on December 31, 2008 due to a leap year bug. It’s possible to find segments of the source code that caused this problem on the Internet. Essentially, the clock code for the Zune behaved correctly on any day of the year numbered 365 or lower. Likewise, when the day was greater than 366, it would correctly move to the next year and reset the day counter. When day was exactly 366, however, the Zune became stuck in an infinite loop. What kind of testing should Microsoft have done to prevent this bug?

Programming Practice

  1. Apply JUnit testing to the last major assignment you did in class. What bugs did you uncover?

Experiments

  1. James Gosling’s original specification for Java contained assertions, but they weren’t included until Java 1.4. One of the concerns about an assertion mechanism is the additional time required to process the assertions. Time a program of at least moderate length before adding assert statements to its methods. If you use assert statements to check method input and output thoroughly, you should see a slight decrease in performance when assertions are enabled. When disabled, you should see almost none. How great is the performance hit?

  2. Take another look at your last programming assignment. Calculate the number of branches based on if and switch statements and compute 2 raised to that power. Time your program executing once under normal circumstances. Multiply that time by the number of different possibilities you would need to exercise every possible combination of branches in your program. How much total time would it take to run all of those different executions of your program?

  3. Take a concurrent program you’ve written that relies on explicit synchronization mechanisms for correctness. Remove all synchronization tools and run the code many times, testing for race conditions. How many runs does it take before a race condition causes an error? If you can, run your program on systems with different numbers of cores. Using a larger number of cores should make race conditions more evident since more processors can execute contentious code in parallel.

17. Polymorphism

…​how strange it is to be anything at all.

— Neutral Milk Hotel

17.1. Problem: Banking account with a vengeance

In Chapter 14, we introduced the SynchronizedAccount class that guarantees that checking the balance, making deposits, and making withdrawals will all be safe even in a multi-threaded environment. Unfortunately, SynchronizedAccount provides few of the options a full bank account should have. The problem we present to you now is to create an entire line of bank accounts which all inherit from SynchronizedAccount. Because of inheritance, all accounts will at least have getBalance(), deposit(), and withdraw() methods.

You must create three new account classes. The constructor for each class must take a String which gives the name of the person opening the account and a double which gives the starting balance of the account. The first of these classes is CheckingAccount. The rules for the checking account implemented by this class are simply that the customer is charged $10 every month that the account is open. The second class is DirectDepositAccount. This account is very similar to the basic checking account except that an additional method directDeposit() has been added. On its own, directDeposit() appears to operate like deposit(); however, if a direct deposit has been made in the last month, no service fee will be charged to the account.

The SavingsAccount class operates somewhat differently. In addition to a name and a starting balance, the constructor for a SavingsAccount takes a double which gives the annual interest rate the account earns. Each month the balance is checked. If the balance of the account is greater than $0, the account earns interest corresponding to 1/12 of the annual rate. However, if the balance is below $1,000, a $25 service fee is charged each month, regardless of how low the balance becomes.

In Chapter 11 you were exposed to concepts surrounding inheritance in object-oriented programming. We now return to these concepts and explore them further. In the first place, concurrency is on the table now, and you must be careful to keep your derived classes thread safe. In the second, we’ll discuss the full breadth of inheritance. The tools we describe here are intended to allow you to solve this extended bank account problem (and indeed many other problems) with as little code as possible.

17.2. Concepts: Polymorphism

Perhaps the most important reason for inheritance is code reuse. When you can successfully reuse existing code, you’re not just saving the time of writing new code. You’re also leveraging the quality and correctness of the existing code. For example, when you create your own class which extends Thread, you’re confident that all the thread mechanisms work properly.

You can reuse code by taking a class that does something you like, say the Racecar class, and enhance it in some way, perhaps so that it becomes the TurboRacecar class. If you use the TurboRacecar class on its own, your code reuse is through simple inheritance. If you use TurboRacecar objects with a RaceTrack class, which was written to take Racecar objects as input, you’ve entered the realm of polymorphism. Polymorphism means that the same method can be used on different types of objects without being rewritten. In Java, polymorphism works by allowing the programmer to use a child class in any place where a parent class could have been used.

17.2.1. The is-a relationship

Consider the two following class definitions.

public class Racecar {
    public double getTopSpeed() { return 200.0; }   
    public int getHorsepower() { return 700; }
    public double speed = 0;
}
public class TurboRacecar extends Racecar {
    public int getHorsepower() { return 1100; }
}

Now, imagine that a RaceTrack has an addCar(Racecar car) method which adds a Racecar to the list of cars on the track. When the cars begin racing, the RaceTrack object will query the cars to see how much horsepower they have. A Racecar object will return 700 when getHorsepower() is called, but a TurboRacecar will return 1100.

Even through the TurboRacecar doesn’t have an explicit getTopSpeed() method, it inherits one from Racecar. Like all derived classes in Java, TurboRacecar has all the methods and fields that Racecar does. This relationship is called an is-a relationship because every TurboRacecar is a Racecar in the sense that you can use a TurboRacecar whenever a Racecar is required.

17.2.2. Dynamic binding

There’s a little bit of magic that makes polymorphism work. When you compile your code, the RaceTrack doesn’t know which getHorsePower() method will eventually get called. Only at run time does it query the object in question and, if it’s a TurboRacecar, use the overridden method that returns 1100. This feature of Java is called dynamic binding. Not every object oriented programming language supports dynamic binding. C++ actually allows the programmer to specify whether or not a method is dynamically bound.

Only methods are dynamically bound in Java. Fields are statically bound. Consider the following re-definitions of Racecar and TurboRacecar.

public class StaticRacecar {
    public static final double TOP_SPEED = 200.0;   
    public static final int HORSEPOWER = 700;
    public double speed = 0;
}
public class StaticTurboRacecar extends StaticRacecar {
    public static final int HORSEPOWER = 1100;
}

Assume that RaceTrack contains a method which prints out the horsepower of a StaticRacecar like so:

public void printHorsepower(StaticRacecar car) {
    System.out.print(car.HORSEPOWER);
}

Even if you pass an object of type StaticTurboRacecar to the printHorsepower() method, the value 700 will be printed out every time. In Java, all fields, whether normal, static, or final are statically bound, meaning that the variable type determines which field will be used at compile time. In contrast, dynamic binding means that the true type of the object (not the reference variable pointing to it) determines the method that will be called at run time. Dynamic binding applies only to regular instance methods. As their name implies, static methods are statically bound, determined at compile time by reference type.

17.2.3. General vs. specific

Another way to look at inheritance is as a statement of specialization and generalization. A TurboRacecar is a specific kind of Racecar while Racecar is a general category that TurboRacecar belongs to.

The rules of Java say that you can always use a more specific version of a class than you need but never a more general one. You can use a TurboRacecar any time you need a Racecar but never use a Racecar when you need a TurboRacecar. A square will do the job of a rectangle, but a rectangle will not always be suitable when a square is needed.

Consider the following two classes:

public class Vehicle {
	public void travel(String destination) {
		System.out.println("Traveling to " + destination + "!");
	}
}
public class RocketShip extends Vehicle {
	public void blastOff() {
		System.out.println("10, 9, 8, 7, 6, 5, 4, 3, 2, 1 *ROAR*");
	}
}

Here’s a method that requires a RocketShip but only uses its travel() method.

public void takeVacation(RocketShip ship, String destination) {
    ship.travel(destination);
}

It seems as though we should be able to pass any Vehicle to the takeVacation() method because the only method in ship used by takeVacation() is travel(). However, the programmer specified that the parameter should be a RocketShip, and Java plays it safe. Just because it looks like there won’t be a problem, Java isn’t going to take any chances on passing an overly general Vehicle when a RocketShip is required. If Java took chances, a problem could arise if the takeVacation() method was overridden by a method that did call ship.blastOff().

In summary, you can pass a RocketShip to a method which takes a Vehicle or store a RocketShip into an array of Vehicles, but not the reverse. Java usually gives a compile time error if you try to put something too general into a location that’s too specific, but there are some situations which are so tricky that Java doesn’t catch the error until run time. Arrays, specifically, can cause problems. Examine the following code snippet.

Vehicle[] transportation = new RocketShip[100];
transportation[0] = new Vehicle();

On the first line, we’re using a Vehicle array reference to store an array of 100 RocketShip references. But, in the second line, we try to store a Vehicle into an array that’s really a RocketShip array, even though it looks to the compiler like a Vehicle array. Doing so will compile but throw an ArrayStoreException at run time.

17.3. Syntax: Inheritance tools in Java

So far, we’ve described polymorphism in Java with a conceptual focus. In our previous examples, the only language tool needed to use polymorphism was the extends keyword which you’re now well familiar with. There are a number of other tools designed to help you structure class hierarchies and enforce design decisions.

17.3.1. Abstract classes and methods

One such tool is abstract classes. An abstract class is one which can never be instantiated. In order to use an abstract class, it’s necessary to make a child class from it. To create an abstract class, you simply add the abstract keyword to its definition, as in the following example.

public abstract class Useless {
    protected int variable;
    
    public Useless(int input) {
        variable = input;
    }
    
    public void print() {
        System.out.println(variable);
    }
}

This class is useless for a number of reasons. For one thing, there’s no way to find out the value of variable except by printing it out. Furthermore, there’s no way to change the value of variable after the object’s been created. Finally, since an abstract class can’t be instantiated, the following code snippet will not compile.

Useless thing = new Useless(14);

Instead, we must create a new class that extends Useless.

public class Useful extends Useless {
	public Useful(int value) {
		super(value);
	}
	
    public int getVariable() { return variable; }
    
    public void setVariable(int value) {
        variable = value;
    }
}

Then, we can instantiate an object of type Useful and use it for something.

Useless item = new Useful(14);
item.print();

Note that, in accordance with the rules of Java, we can store an object with the more specific type Useful into a reference with the more general type Useless. Even though Java knows that the object it points to will never actually be a Useless object, it’s perfectly legal to have a Useless reference. You can use abstract classes in this way to provide a base class with some fundamental fields and methods that all other classes in a particular hierarchy need. By using the keyword abstract, you’re marking the class as template for other classes instead of as a class that will be used directly.

Methods can be abstract as well. If you have an abstract class, you can create a method header which describes a method that all non-abstract children classes must implement, as shown below.

public abstract class Sequence {
    protected int number;
    protected final int CONSTANT;
    
    public Sequence(int number, int constant) {
        this.number = number;
        CONSTANT = constant;
    }
    
    public abstract int getNextValue();
}

This abstract class is supposed to be a template for classes which can produce some sequence of numbers. Note that there’s no body for the getNextValue() method. It simply ends with a semicolon. Every non-abstract derived class must implement a getNextValue() method to produce the next number in the sequence. For example, we could implement an arithmetic or a geometric sequence as follows.

public class ArithmeticSequence extends Sequence {
	public ArithmeticSequence(int firstTerm, int difference) {
		super(firstTerm, difference);
	}	
	
    public int getNextValue() {
        number += CONSTANT;
        return number;
    }
}
public class GeometricSequence extends Sequence {
	public GeometricSequence(int firstTerm, int ratio) {
		super(firstTerm, ratio);
	}
	
    public int getNextValue() {
        number *= CONSTANT;
        return number;
    }
}

The Sequence class doesn’t specify how the sequence of numbers should be generated, but any derived class must implement the getNextValue() method in order to compile. By using an abstract class, we don’t have to create a base class which generates a meaningless sequence of numbers just for the sake of establishing the getNextValue() method. Abstract classes are like interfaces in that they can force the programmer who’s extending them to implement particular methods, but unlike interfaces they can also define fields and methods of any kind.

Example 17.1 Bank account abstract class

Here’s a more involved example of an abstract class that gives a first step toward solving the bank account with a vengeance problem posed at the beginning of the chapter.

import java.util.Calendar; (1)

public abstract class BankAccount extends SynchronizedAccount { (2)
    private String name;
    private Calendar lastAccess;
    private int monthsPassed = 0;
1 The first step is to import the Calendar class for some date tools we’re going to use later.
2 We extend SynchronizedAccount and declare the new class to be abstract. In this example, we don’t use any abstract methods, but since each bank account has unique characteristics, we don’t want people to be able to create a generic BankAccount.
    public BankAccount(String name, double balance) throws InterruptedException {
        this.name = name;
        changeBalance(balance);
        lastAccess = Calendar.getInstance();        
    }
    
    public String getName() { return name; }
    
    protected Calendar getLastAccess() { return lastAccess; }
    
    protected int getMonthsPassed() { return monthsPassed; }

The constructor and the accessors should be what you expect to see. Note that calling the static method Calendar.getInstance() is the correct way to get a Calendar object with the current date and time.

    public final double getBalance() throws InterruptedException {      
        update();       
        return super.getBalance();
    }
    
    public final void deposit(double amount) throws InterruptedException {
        update();
        super.deposit(amount);        
    }
    
    public final boolean withdraw(double amount) throws InterruptedException {
        update();       
        return super.withdraw(amount);
    }

Then come the balance checking and changing methods. Each calls its parent method after calling an update() method we discuss below.

    protected synchronized void update() throws InterruptedException {
        Calendar current = Calendar.getInstance();
        int months = 12*(current.get(Calendar.YEAR) -
			lastAccess.get(Calendar.YEAR)) + (current.get(Calendar.MONTH) -
			lastAccess.get(Calendar.MONTH));
        if(months > 0) {
			lastAccess = current;
			monthsPassed = months;
        }
    }
}   

Other than adding String for a name associated with the account, the update() method is the other major addition made in BankAccount. Each time update() is called, the number of months since the last access is stored in the field monthsPast and the timestamp of the last access is stored in lastAccess. We didn’t need these time features before, but issues like earning interest or paying monthly service charges will make them necessary. This method is synchronized so that the two fields associated with the last access are updated atomically.

17.3.2. Final classes and methods

If you look at the previous example carefully, you’ll notice that the methods getBalance(), deposit(), and withdraw() were each declared with the keyword final. You’ve seen this keyword used to declare constants before. The meaning that final has for methods is similar to what it means for constants (and almost the opposite of abstract). A method which is declared final can’t be overridden by child classes. If you’re designing a class hierarchy and you want to lock a method into doing a specific thing and never changing, this is the way to do it.

Like abstract, the keyword final can be applied to a class as well. If you want to prevent a class from being extended further, apply the final keyword to its definition. You might not find yourself using this feature of Java very often. It’s primarily useful in situations where a large body of code has been designed to make use of a specific class. Making child classes from that class could cause unexpected problems.

The most common example of a final class is the String class. Consider the following.

public class SuperString extends String {}

This code will give a compiler error. String is perfect the way it is (or so the Java designers have decided). Use of the final keyword for classes, methods, and especially to specify constants allows the compiler to do performance optimizations that would otherwise be impossible.

17.3.3. Casting

Polymorphism gives us greater power and flexibility when writing code. For example, we can make a Vehicle array and store child class objects of Vehicle inside, like so.

Vehicle[] vehicles = new Vehicle[5];
vehicles[0] = new Skateboard();
vehicles[1] = new RocketShip();
vehicles[2] = new SteamBoat();
vehicles[3] = new Car();
vehicles[4] = new Skateboard;

This process could be infinitely more complex. We could be reading data out of a file and dynamically creating different kinds of Vehicle objects, but the final product of an array of Vehicle objects is the important thing. Now, we can run through the array with a loop and have the code magically work for each kind of Vehicle.

for(int i = 0; i < vehicles.length; i++)
    vehicles[i].travel("Prague");

Each Vehicle will travel to Prague as it should. The only trouble is that we’ve hidden some information from the compiler. We know that vehicles[1] is a RocketShip, but we can’t treat it like one.

vehicles[1].blastOff();

This code won’t compile.

RocketShip ship = vehicles[1];

This code won’t compile either. In both cases, we must use an explicit cast to tell the compiler that the object really is a RocketShip.

((RocketShip)vehicles[1]).blastOff();
RocketShip ship = (RocketShip)vehicles[1];

Now, both lines of code work. The compiler is always conservative. It never makes guesses about the type of something. Consider the following.

Vehicle ship = new RocketShip();
ship.blastOff();

Even though ship must be a RocketShip, Java doesn’t assume that it is. The compiler uses the reference type Vehicle to do the check and will refuse to compile. Casting allows us to use our human insights to overcome the shortsightedness of the compiler. Unfortunately, there’s no guarantee that human insights are correct. What happens if you cast improperly?

Vehicle vehicle = new Skateboard();
RocketShip ship = (RocketShip)vehicle;
ship.blastOff();

In this example, we’re trying to cast a Skateboard into a RocketShip. At compile time, no errors will be found. Because we use explicit casting, the compiler assumes that we, powerful human beings that we are, know what we’re doing. The error will happen at run time while executing the second line. Java will try to cast vehicle into a RocketShip, fail, and throw a ClassCastException.

Java provides some additional tools to make casting easier. One of these is the instanceof keyword which can be used to test if an object is an instance of a particular class (or one of its derived classes). For example, we can make an object execute a special command if we know that the object is capable of it.

public void visitDenver(Vehicle vehicle) {
    if(vehicle instanceof RocketShip)
        ((RocketShip)vehicle).blastOff();
    vehicle.travel("Denver");
}

Even inside this if statement where it must be the case that vehicle is a RocketShip, we still must perform an explicit cast. Sometimes instanceof is not precise enough. If you must be sure that the object in question is a particular class and not just one of its child classes, you can use the getClass() method on any object and compare it to the static class object. Using this tool, we can rewrite the former example to be more specific.

public void visitDenver(Vehicle vehicle) {
    if(vehicle.getClass() ==  RocketShip.class)
        ((RocketShip)vehicle).blastOff();
    vehicle.travel("Denver");
}

This version of the code will only call blastOff() for objects of class RocketShip and not for objects of a child class like FusionPoweredRocketShip.

17.3.4. Inheritance and exceptions

Beyond ClassCastException, there are a few other issues that come up when combining exceptions with inheritance. As you already know, an exception handler for a parent class will work for a child class. As such, when using multiple exception handlers, it’s necessary to order them from most specific to most general in terms of class hierarchy.

However, there’s another subtle rule that’s necessary to keep polymorphism functioning smoothly. Let’s consider a Fruit class with an eat() method that throws an UnripeFruitException.

public class Fruit {
    public void eat() throws UnripeFruitException {
        ...
    }
}

Almost any fruit can be unripe, and it can be unpleasant to try to eat such a fruit. But there are other things that can go wrong when eating fruit. Consider the Plum class derived from Fruit.

public class Plum extends Fruit {
    public void eat() throws UnripeFruitException, ChokingOnPitException {
        ...
    }
}

In the Plum class, the eat() method has been overridden to tackle the special ways that eating a plum is different from eating fruit in general. When eating a plum, you can make a mistake and try to swallow the pit, throwing, it seems, a ChokingOnPitException. This scenario seems natural, but it’s not allowed in Java.

The principle behind polymorphism is that a more specialized version of something can always be used in place of a more general version. Indeed, if you use a Plum in place of a Fruit, calling the eat() method is no problem. The problem only happens if a ChokingOnPitException is thrown. Code that was designed for Fruit objects knows nothing about a ChokingOnPitException, so there’s no way for such code to catch the exception and deal with the situation.

There’s nothing wrong with throwing exceptions on overridden methods. The rule is that the overriding method must throw a subset of the exceptions that the overridden method throws. This subset doesn’t need to be a proper subset, so it could be all, some, or none of the exceptions thrown by the overridden method. This rule demonstrates a concept called Hoare’s rule of consequence that pops up many times in programming language design. Essentially, if you start with something that works, you can tighten the requirements on the input (using a Plum instead of any Fruit) and loosen the requirements on the output (throwing fewer exceptions than were originally thrown), and it will still work.

Example 17.2 More human than human

Here we have a few additional examples in a somewhat larger class hierarchy.

animals
Figure 17.1 Animal class hierarchy.
public abstract class Animal {
    private boolean alive = true;
    private boolean happy = true;
    private final boolean warmBlooded;
    
    public Animal(boolean warmBlooded) {      
        this.warmBlooded = warmBlooded;
    }   
        
    public boolean isHappy() { return happy; }  
    
    public void setHappy(boolean value) { happy = value; }        
    
    public boolean isAlive() { return alive; }  
    
    public void die() { alive = false; }    
}

We begin with the abstract Animal class. This class gives a base definition for animals which includes whether the animal is alive, whether the animal is happy, and whether it’s warm-blooded (declared final because an animal can’t switch between warm-blooded and cold-blooded).

public abstract class Mammal extends Animal {
    public static final boolean MALE = false;
    public static final boolean FEMALE = true;
    private boolean gender;
    
    public Mammal(boolean gender) {
        super(true);
        this.gender = gender;
    }
    
    public boolean getGender() { return gender; }   

    public abstract String makeSound();
}

We then extend Animal into Mammal. All mammals are warm-blooded, which is reflected in the constructor call to the base class. In addition, it’s assumed that all mammals make some sound. Mammals generally also have well-defined genders. Like Animal, Mammal is an abstract class, and any non-abstract child of Mammal must implement makeSound().

public class Platypus extends Mammal {
    public Platypus(boolean gender) {
        super(gender);
    }

    public String makeSound() {
        return "Quack!";
    }   
    
    public Egg layEgg() {
        if(getGender() == FEMALE)
            return new Egg();
        else
            return null;
    }
    
    public void poison(Animal victim) {
        if(getGender() == MALE)
            victim.setHappy(false);                           
    }
}

The Platypus class extends Mammal and adds the unusual things that a platypus can do: laying eggs (if female) and poisoning other animals (if male).

public class Human extends Mammal {
    public Human(boolean gender, boolean happy) {
        super(gender);
        setHappy(happy);
    }

    public String makeSound() {
        return "Hello, world.";
    }
}

The Human class also extends Mammal. Depending on the problem being solved, this class might warrant a great deal more specialization. Right now the main addition is taking happiness as an argument to the constructor since the default human state is not necessarily happiness.

public final class DavidBowie extends Human {
    public DavidBowie() {
        super(MALE, true);                
    }

    public String makeSound() {
        return "I always had a repulsive need to be something more than human.";
    }
}

Finally, the DavidBowie class extends Human and is declared a final class because it’s impossible to add anything to David Bowie.

Our examples have stretched fairly long in this chapter. It’s difficult to give strong motivation for some aspects of inheritance and polymorphism without a large class hierarchy. These tools are designed to help organize large bodies of code and should become more useful as the size of the problem you’re working on grows. One of the best examples of the success of inheritance is the Java API itself. The standard Java library is large and depends on inheritance a great deal.

17.4. Solution: Banking account with a vengeance

Now, we return to the specific problem given at the beginning of the chapter and give its solution. We’ve already given you the BankAccount abstract class which provides a lot of structure.

accounts
Figure 17.2 Bank account class hierarchy.
Program 17.1 Child class of BankAccount that models a normal checking account.
public class CheckingAccount extends BankAccount {
    public static final double FEE = 10;
    
    public CheckingAccount(String name, double balance)
        throws InterruptedException {
        super(name, balance);             
    }
    
    protected synchronized void update() throws InterruptedException {
        super.update();
        changeBalance(-getFee()*getMonthsPassed());
    }
    
    protected double getFee() { return FEE; }
}

The most basic account is the CheckingAccount. As you recall from the BankingAccount class, the getBalance(), deposit(), and withdraw() methods are all declared final. At first it seems as if there’s no way to change these methods to add the $10 service charge. However, each of those methods calls the update() method first to take care of any bookkeeping. By overriding the update() method, we can easily add the service charge. The new update() method calls the parent update() to calculate the passage of time, then it changes the balance based on the number of months that have passed.

The system we’ve adopted may seem unusual at first. Any time the balance is checked, deposited to, or withdrawn from, we call update(). By updating the account to reflect any months which may have passed before continuing on, we don’t have to write code which periodically updates each bank account. Each bank account is only updated if needed.

We were careful to mark update() as synchronized. Although the chance of an error happening is small, we make the update of the internal Calendar and the application of any fee atomic, just to be safe.

Note that we don’t use the constant FEE directly in update(). Instead, we call the getFee() method. The reason for this decision is due to the next class.

Program 17.2 Child class of CheckingAccount that models the behavior of accounts with direct deposits.
import java.util.Calendar;
public class DirectDepositAccount extends CheckingAccount {
    protected Calendar lastDirectDeposit;
    
    public DirectDepositAccount(String name, double balance)
        throws InterruptedException {
        super(name, balance);
        lastDirectDeposit = Calendar.getInstance();
    }
    
    public double getFee() {        
        Calendar current = Calendar.getInstance();
        int months = 12*(current.get(Calendar.YEAR) -
        lastDirectDeposit.get(Calendar.YEAR)) +
        (current.get(Calendar.MONTH) -
        lastDirectDeposit.get(Calendar.MONTH));
        if(months <= 1)
            return 0;
        else
            return super.getFee();
    }
    
    public void directDeposit(double amount) throws InterruptedException {
        deposit(amount);
        lastDirectDeposit = Calendar.getInstance();
    }
}

The DirectDepositAccount class extends the CheckingAccount class. Note that the update() method hasn’t been overridden. We’ve added another Calendar object to keep track of the last time a direct deposit was made. Then, we override the getFee() method. If there’s been a recent direct deposit, the fee is nothing; otherwise, it returns the fee from the CheckingAccount. Because of dynamic binding, the update() method defined in CheckingAccount will call this overridden getFee() method for DirectDepositAccount objects.

Program 17.3 Child class of BankAccount that models the behavior of a savings account.
public class SavingsAccount extends BankAccount {
    public static final double MINIMUM = 1000;
    public static final double FEE = 25;
    protected final double RATE;

    public SavingsAccount(String name, double balance, double rate)
        throws InterruptedException {
        super(name, balance);
        RATE = rate;        
    }
    
    protected double getFee() { return FEE; }
    
    protected double getMinimum() { return MINIMUM; }
    
    protected synchronized void update() throws InterruptedException {
        super.update(); 
        int months = getMonthsPassed();
        for(int i = 0; i < months; i++) {
            if(getBalance() > 0)
                changeBalance(getBalance() * (1 + RATE/12));
            if(getBalance() < getMinimum())
                changeBalance(-getFee());
        }               
    }
}

There should be few surprises in the last class, SavingsAccount. The biggest difference is that we use a loop in the update() method to update the balance because the account could be gaining interest and also incurring fees. The interaction of the two operations might give a different result than applying each in a block for the backlog of months.

This set of classes might not resemble the way a real, commercial-grade banking application works. Nevertheless, with inheritance and polymorphism we were able to create bank accounts which do some complicated tasks with a relatively small amount of code. At the same time, we preserved thread safety so that these accounts can be used in concurrent environments.

17.5. Concurrency: Atomic libraries

This chapter has discussed using polymorphism to reuse code. To solve the banking account with a vengeance problem from the beginning of the chapter, we explored the process of extending several bank account classes to add additional features while working hard to maintain thread safety.

Code can be reused by extending classes with child classes or by using instances of existing classes as fields. There’s no single solution that’s best for every case. As in the bank account examples, it can be difficult to know when to apply the synchronized keyword to methods.

To lessen the load on the programmer, the Java API provides a library of atomic primitives in the java.util.concurrent.atomic package. These are classes with certain operations guaranteed to execute atomically. For example, the AtomicInteger class encapsulates the functionality of an int variable with atomic accesses. One of its methods is incrementAndGet(), which will atomically increment its internal value by 1 and return the result. Recall from Program 14.1 that even an operation as simple as ++ isn’t atomic. If many different threads try to increment a single variable, some of those increments can get lost, causing the final value to be less than it should be.

Example 17.3 AtomicInteger

We can use the AtomicInteger class to rewrite Program 14.1 so that no race condition occurs.

Program 17.4 Demonstrates the use of AtomicInteger.
import java.util.concurrent.atomic.*;

public class NoRaceCondition extends Thread {       
    private static AtomicInteger counter = new AtomicInteger(); 
    public static final int THREADS = 4;    
    public static final int COUNT = 1000000;        
    
    public static void main(String[] args) {                              
        NoRaceCondition[] threads = new NoRaceCondition[THREADS];           
        for(int i = 0; i < THREADS; i++) {
            threads[i] = new NoRaceCondition();
            threads[i].start();         
        }           
        try {
            for(int i = 0; i < THREADS; i++)
                threads[i].join();
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }           
        System.out.println("Counter:\t" + counter.get());          
    }   
    
    public void run() { 
        for(int i = 0; i < COUNT / THREADS; i++)
            counter.incrementAndGet();
    }
}

This program is identical to Program 14.1, except that the type of counter has been changed from int to AtomicInteger (and an appropriate import has been added). Consequently, the ++ operation was changed to an incrementAndGet() method call, and a get() method call was needed to get the final value. If you run this program, the final answer should always be 1,000,000, no matter what.

The java.util.concurrent.atomic package includes AtomicBoolean and AtomicLong as well as AtomicInteger. Likewise, the AtomicIntegerArray and AtomicLongArray classes are included to perform atomic array accesses. For general purposes, the AtomicReference<V> class provides an atomic way to store a reference to any object. The <V> is a generic type parameter, which will be discussed in Chapter 18.

Although you could use the synchronized keyword to create each one of these classes yourself, the result wouldn’t be as efficient. The atomic classes use a special lock-free mechanism. Unlike the synchronized keyword which forces a thread to acquire a lock on a specific object, lock-free mechanisms are built on a compare-and-swap (CAS) hardware instruction. Thus, incrementing and the handful of other ways to update an atomic variable execute in one step because of special instructions on the CPU. Since there’s no waiting to acquire a lock or fighting over which thread has the lock, the operation is very fast. Many high performance concurrent applications depends on CAS implementations.

17.6. Exercises

Conceptual Problems

  1. Explain the difference between static binding and dynamic binding. In which situations does each apply?

  2. Consider the following two classes.

    public class Sale {
        public double discount = 0.25;
        
        public double getDiscount() {
            return discount;
        }
        
        public void setDiscount(double value) {
            discount = value;
        }
    }
    public class Blowout extends Sale {
        public double discount = 0.5;
        
        public double getDiscount() {
            return discount;
        }
        
        public void setDiscount(double value) {
            discount = value;
        }
    }

    Given the following snippet of code, what’s the output and why?

    Sale sale = new Blowout();
    System.out.println(sale.discount);
    System.out.println(sale.getDiscount);
    Blowout blowout = (Blowout)sale;
    System.out.println(blowout.discount);
    sale.setDiscount(0.75);
    System.out.println(sale.discount);
  3. What are the differences and similarities between abstract classes and interfaces?

  4. Assume that the Corn, Carrot, and Potato classes are all derived from Vegetable. Both Carrot and Potato classes have a peel() method, but Corn does not. Examine the following code and identify which line will cause an error and why.

    Vegetable[] vegetables = new Vegetable[30];
    for(int i = 0; i < vegetables.length; i += 3) {
        vegetables[i] = new Corn();
        vegetables[i + 1] = new Carrot();
        vegetables[i + 2] = new Potato();
    }
    int index = vegetables.length - 1;
    Potato potato;
    while(index >= 0) {
        potato = (Potato)vegetable[index];
        potato.peel();
    	index--;
    }
  5. How many different structures can the keyword final be applied to in Java, and what does final mean in each case?

  6. Assume that Quicksand is a child class of Danger.

    What’s the output of the following code and why?

    Quicksand quicksand = new Quicksand();
    if(quicksand instanceof Danger) {
    	System.out.printf("Run for your lives!");
    if(quicksand.getClass() == Danger.class)
    	System.out.printf("Run even faster!");
    if(quicksand instanceof Quicksand) {
    	System.out.printf("The more you struggle, the faster you'll sink!");
    if(quicksand.getClass() == Quicksand.class)
    	System.out.printf("You'll need to find a vine to escape!");
  7. Consider the following two classes. What’s the problem that prevents compilation?

    public class Snake {
    	public void handle() throws BiteException {
    		System.out.println("You handled a snake!");
    		if(Math.random() > 0.9)
    			throw new BiteException();
    	}
    }
    public class Cobra extends Snake {
    	public void handle() throws BiteException, PoisonException {
    		System.out.println("You handled a cobra!");
    		if(Math.random() > 0.8) {
    			if(Math.random() > 0.2)
    				throw new PoisonException();
    			throw new BiteException();
    		}
    	}
    }

Programming Practice

  1. Update the solution from Section 11.5 so that it uses as many of the inheritance tools from this chapter as possible. Clearly, the Gate, UnaryOperator, and BinaryOperator classes should be marked abstract. Which methods should be abstract? Which methods or classes should be final?

  2. Implement a program to assess income tax on normal employees, students, and international students using a class hierarchy. Normal employees pay a 6.2% social security tax and a 1.45% Medicare tax every year, but neither kind of student pays these taxes. All three groups pay normal income tax according to the following table.

    Marginal Tax Rate Income Bracket

    10%

    $0 - $9,700

    12%

    $9,701 - $39,475

    22%

    $39,476 - $84,200

    24%

    $84,201 - $160,725

    32%

    $160,726 - $204,100

    35%

    $204,101 - $510,300

    37%

    $510,301+

    Tax is assessed at a given rate for every dollar in the range. For example, if someone makes $35,000, she pays 10% tax on the first $9,700 of her income and 12% on the remaining $25,300. The exception is international students whose country has a treaty with the U.S. so that they don’t have to pay tax on the first $50,000 of income.

  3. Re-implement the original SynchronizedAccount class from Example 14.2 using atomic classes. For simplicity, you can change the balance type from double to AtomicInteger since there’s no AtomicDouble class. How much has this simplified the implementation? Is the readers field still necessary? Why or why not?

Experiments

  1. Take Program 17.4 and increase COUNT to 100000000. Run it several times and time how long it takes to run to completion.

    Then, take Program 14.1 and increase its COUNT to 100000000 as well. Change the body of the for loop inside the run() method so that count++; is inside of a synchronized block that uses RaceCondition.class as the lock. (The choice of RaceCondition.class is arbitrary, but it’s an object that all the threads can see.) In this way, the increment will occur atomically, since only the thread that has acquired the RaceCondition.class lock will be able to do the operation. Now, run this modified program several times and time it.

    How different are the running times? They might be similar, depending on the implementation of locks and CAS on your OS and hardware platform.

18. Dynamic Data Structures

Algorithms + Data Structures = Programs

— Niklaus Wirth

18.1. Problem: Infix conversion

If a math teacher writes the expression 1 + 7 × 8 - 6 × (4 + 5) ÷ 3 on the blackboard and asks a group of 10-year-old children to solve it, different children might give different answers. The difficulty lies not in the operations themselves but in the order of operations. Modern graphing calculators and many computer programs can, of course, evaluate such expressions correctly, but how do they do it? You intuitively grasp order of operations (left to right, multiplication and division take higher precedence than addition and subtraction, and so on), but encoding that intuition into a computer program requires effort.

One way a computer scientist might approach this problem is to turn the mathematical expression from one that’s difficult to evaluate into one that’s easy. The normal style of writing mathematical expressions is called infix notation, because the operators are written in between the operands they’re used on. An easier notation for automatic evaluation is called postfix notation, because an operator is placed after the operands it works on. The following table gives a few examples of expressions written in both infix and postfix notation.

Infix Postfix

3 + 7

3 7 +

4 * 2

4 2 *

1 + 9 * 2

1 9 2 * +

(1 + 9) * 2

1 9 + 2 *

Although infix notation is probably more familiar to you, postfix notation has the benefit of exactly specifying the order of operations without using any precedence rules and without needing parentheses to clarify. To understand how to compute an expression in postfix notation, we rely on the idea of a stack, which we first introduced in Chapter 9 and examine in greater depth here.

Recall that a stack has three operations: push, pop, and top. It works like a stack of physical objects. The push operation places an object on the top, the pop operation removes an item from the top, and the top operation tells you what’s at the top of the stack.

Using a stack, postfix evaluation rules are easy: Scan the expression from left to right, if you see an operand (a number, in our case), put it on the stack. If you see an operator, pop the last two operands off the stack and use the operator on them. Then, push the result back on the stack. When you run out of input, the value at the top of the stack is your answer.

For example, with 1 9 2 * +, all three operands are pushed onto the stack. Then, the * is read, and so 2 and 9 are popped off the stack and multiplied. The result 18 is pushed back on the stack. Then, the + is read, and 18 and 1 are popped off the stack and summed, resulting in 19, which is pushed back on the stack as the final answer.

Our problem, however, is not to evaluate an expression in postfix notation but to convert an expression in infix notation to postfix notation. Again, the concept of a stack is useful. To do the conversion, we initialize a stack and scan through the input in infix notation. As we scan through the input, we do one of four things depending on which of the four possible inputs we see.

Operand

Simply copy the operand to the postfix output.

Operator

If the stack’s empty, push the operator onto the stack. If the stack’s not empty and the operator at the top of the stack has the same or greater precedence than our new operator, put the top operator into our postfix output and pop the stack. Continue this process as long as the top operator has the same or greater precedence compared to our new operator and the stack is not empty. Finally, push the new operator onto the stack.

Left Parenthesis

Push the left parenthesis onto the stack.

Right Parenthesis

Pop everything off the stack and add it to the output until you reach a left parenthesis on the stack. Then pop the left parenthesis.

Precedence comes from order of operations: * and / have high precedence and ` and `-` have low precedence. When you encounter it on the stack, treat `(` as if it has even lower precedence than ` and -. A right parenthesis should never appear on the stack.

Following this algorithm, we’re able to write a program that converts infix notation to postfix notation. We further restrict our problem to the case when the only operands are positive integers in the range [0, 9]. Since each is a single character, parsing the input is much easier. The same ideas for postfix conversion hold no matter how the input is formatted, but parsing arbitrarily formatted numbers is a difficult problem in its own right. This restriction also makes spaces unnecessary.

To solve the infix conversion problem, we need to create a stack data structure whose elements are terms from an infix expression, where a term is an operator, operand, or a parenthesis. We created a stack to solve the nesting expression problem in Section 9.1, but we explore stacks in this chapter as one of many different kinds of dynamic data structure.

18.2. Concepts: Dynamic data structures

By now you’ve seen several ways to organize data in your programs. For example, you’ve used arrays to store a sequence of values and class definitions to store (and operate on) collections of related values in objects. These data structures have the property that they’re static in size: Once allocated, they do not grow as the program runs. If you allocate an array to store 100 integers, you’ll get an error if you try to store 101 integers into it.

In this chapter, we examine dynamic data structures: As more data is read or processed, these data structures grow and shrink in memory to store what is needed. The stack used to solve the nesting expressions problem from Chapter 9 was not actually a dynamic data structure since it was defined with a fixed maximum size. In this chapter, we implement a true stack as well as many other kinds of dynamic data structures.

18.2.1. Dynamic arrays

There are two broad classes of dynamic data structures we examine here. The first kind are based on arrays that grow and shrink. Dynamic arrays allow for fast access to individual elements in the data structure. One drawback of dynamic arrays is that the array that stores that data has a fixed amount of space. When too many elements are added, a new array must to be allocated and all the original elements copied over.

dynamicinsertion
Figure 18.1 Insertion into full dynamic array requiring reallocation. Even if the array wasn’t full, all values after 14 would need to be moved back to insert 17 in order.

Another drawback of dynamic arrays is that they’re poorly suited for insertion or deletion of elements in the middle of the array. When an element is inserted, each element after it must be moved back one position. Likewise, when an element is deleted, all of the elements after it must be moved forward one position. Thus, insertions and deletions at the end of a dynamic array are usually efficient, but insertions and deletions in the middle are time consuming.

18.2.2. Linked lists

The second kind of dynamic data structure is based on objects that link to (or reference) other objects. The simplest form of such a data type is a linked list. A linked list is a data structure made up of a sequence of objects. Each object contains some data value (such as a String) and a link to the next object in the sequence.

linkedlist
Figure 18.2 Visualization of a linked list.

Linked lists are flexible because they have no preset size. Whenever a new element is needed, it can be created and linked into the list. Unfortunately, they can be slow if you need to access arbitrary elements in the list. The only way to reach an element in the list is to walk from element to element until you find what you’re looking for. If the element is at the beginning (or the end) of the list, this process can be quick. If the element is in the middle, there’s no fast way to get there.

Linked lists work well when inserting new elements at arbitrary locations in the list. Unlike arrays, they’re not implemented as a contiguous block of memory. Linking a new element into the middle of the list automatically creates the correct relationship among elements, and there’s no need to move all the elements after an insertion.

Among their downsides is the memory overhead of linked lists. A new object must be allocated for each element in the list, which must include a reference to the next element as well as its own data. Consequently, using a linked list to solve a problem usually take more memory than an equivalent dynamic array solution.

It turns out that either dynamic arrays or linked lists can be used to create an efficient solution to the infix conversion problem defined at the beginning of the chapter.

18.2.3. Abstract data types

The fact that dynamic arrays and linked lists can be used to solve similar problems points out that we may often be more interested in the capabilities of a data structure rather than its implementation.

An abstract data type (ADT) is a set of operations that can be applied to a set of data values with well-defined results that are independent of any particular implementation. In other words, it is a list of things that a data type can do (or have done to it).

A stack is a great example of an ADT. A stack needs to be able to push a value, pop a value, and tell us what value is on top. The internal workings of the stack are irrelevant (as long as they’re efficient). It’s possible to use either a dynamic array or a linked list to implement a stack ADT. A queue is another ADT we discuss in Section 18.4, but there are many other useful ADTs.

18.3. Syntax: Dynamic arrays and linked lists

18.3.1. Dynamic arrays

Suppose you’re faced with the problem of reading a list of names from a file, sorting them into alphabetical order, and printing them out. You’ve already looked at simple sorting algorithms to handle the sorting part, or you could use the Java Arrays.sort() method. In previous problems when you needed to use an array for storing items, you knew in advance how many (or a maximum of how many) items you’d need to store. In this new problem, the number of names in the input file is unspecified, so you must allow an arbitrary number to be handled.

One approach is to make a guess at how many names are in the input file and allocate an array of that size. If your guess is too small, and you don’t check array accesses, you’ll cause an exception once you’ve filled the array and try to store the next name into the index one past the last legal one. If your guess is too large, you could be wasting a significant amount of storage space.

Our first solution to the problem of dealing with dynamic or unknown amounts of data is to watch our array accesses and expand the array as necessary during processing. It’s also possible to contract an array once you determine that the array has more space than needed.

A simple solution

Program 18.1 allocates an array of 10 String references and reads a list of names from standard input until it reaches the end of the file, storing each name in successive array locations. If the number of names in the input is larger than the size of the array, it generates an exception.

Since programs that generate uncaught exceptions are, in general, a bad idea, our first change to this program should be either to catch the exception or check the index before storing the name in the array. In either case, we would then take some action that’s more user friendly than generating an exception, perhaps simply printing an explanatory message before exiting.

Program 18.1 Reads names into an array, sort, and print. If there are more than 10 lines in the input, an exception is generated.
import java.util.Arrays;
import java.util.Scanner;

public class ReadIntoFixedArray {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String[] names = new String[10];
        int count = 0;

        while(in.hasNextLine())
            names[count++] = in.nextLine();
        
        Arrays.sort(names, 0, count);
        
        for(String name: names) (1)
            System.out.println(name);
    }
}
1 Note that we use an enhanced for loop for convenient iteration through the array names. If you don’t recall this syntax, refer to Section 6.9.1.

A second possibility is to take a recovery action that allows the program to proceed. What went wrong? We made a guess of the input size and allocated an array of that size, but our guess was too small. We could modify the code to allocate a larger array at the beginning, recompile, and re-run the program, but that option might not be available to us if the program’s been distributed to users around the world. Instead, we fix the problem on the fly by allocating a larger array, copying the old array into the new array, and continuing.

Program 18.2 begins like the previous program by allocating a fixed-size array.

Program 18.2 Reads names into an array, enlarging the array as necessary.
import java.util.Arrays;
import java.util.Scanner;

public class ReadAndGrowArray {
    public static void main(String[] args) {
        String[] names = new String[10];
        Scanner in = new Scanner(System.in);
        int count = 0;
        String line = null;
        
        while(in.hasNextLine()) {
            line = in.nextLine();
            try {
                names[count] = line;
            }
            catch(ArrayIndexOutOfBoundsException e) { (1)
                names = Arrays.copyOfRange(names, 0, names.length*2); (2)
                names[count] = line;
            }
            count++;
        }
        
        Arrays.sort(names, 0, count);
        
        for(String name: names)
            System.out.println(name);
    }
}
1 The program catches the ArrayOutOfBoundsException if there isn’t enough space for another name in the array.
2 The catch statement allocates a new array, twice the size of the original (current) array, copies the existing array into it, and replaces the reference to the current array with a reference to the new array.

Note that it was necessary to refactor the code in Program 18.1 slightly. We added the line variable to hold the temporary result of reading the input line and moved the count increment outside of the try-catch block.

Can this new, improved program still fail? Yes, but only for very large input, in the case when the Java virtual machine runs out of memory when doubling the size of the array.

A potentially more serious problem is the way we set names to point at a new array.

names = Arrays.copyOfRange(names, 0, names.length*2);

This line works because we know the only variable that references the array is names. If other variables referenced that array, they would continue to reference the old, smaller, and now out-of-date version of the names array. Figure 18.3 shows this problem.

dynamicproblems
Figure 18.3 A poorly designed dynamic array implementation. (a) Both array1 and array2 begin pointing at the same array. (b) A new array has been allocated, and 42 has been added to it. array1 has been updated to point at the new array, but array2 still points at the original.
A more complete solution

The problem of updating variables that reference the dynamic array is a serious issue in large programs. It might not be enough to allocate a larger array and assign the new reference to only one variable. There might be hundreds of variables (or other objects) that reference the original array.

A solution to this problem is to create a new class whose objects contain the array as a private field. References to the array are then mediated, as usual, via accessor methods, which always refers to the same version of the array. Program 18.3 is a simple implementation of a dynamic array class. This class maintains an internal array of String objects, which it extends whenever a call to set() tries to write a new element just past the end of the array.

Program 18.3 Class to manage a dynamic array. This array grows by doubling when more space is needed.
import java.util.Arrays;

public class DynamicArray {
    private String[] strings = new String[10];
   
    public synchronized void set(int i, String string) {
        if(i == strings.length)
            strings = Arrays.copyOfRange(strings, 0, strings.length*2);
        strings[i] = string;
    }
    
    public String get(int i) {
        return strings[i];
    }
    
    public synchronized void sort(int first, int last) {
        Arrays.sort(strings, first, last);
    }
}

Note that the set() and sort() methods are both synchronized in case this class is used by multiple threads simultaneously.

Program 18.4 illustrates how to modify and extend Program 18.1 to use this new class. Since the array grows automatically, there is no need for the original program to check for out-of-bounds exceptions. Of course, the array expansion only works if the reference occurs exactly at the index corresponding to one beyond the end of the array. Other out-of-bound references generate an exception.

Program 18.4 Uses the DynamicArray class to store input read from a file.
import java.util.Scanner;

public class UseDynamicArray {
    public static void main(String[] args) {
        DynamicArray names = new DynamicArray();        
        Scanner in = new Scanner(System.in);
        int count = 0;
        String line = null;
		
        while(in.hasNextLine()) {
            line = in.nextLine();
            names.set(count, line); (1)
            count++;
        }
        
        names.sort(0, count); (2)
        
        for(int i = 0; i < count; i++) (3)
            System.out.println(names.get(i));
    }
}
1 Since names is no longer an array, but rather an object of class DynamicArray, we can no longer use braces ([]) to access elements and must use methods set() and get() instead.
2 Arrays.sort() can’t sort this object directly which is why we provided a sort() method in the class that sorts the private array on demand.
3 We also can’t use an enhanced for loop on names and must iterate through its contents explicitly.

This implementation, like most implementations of dynamic arrays, has potentially serious performance penalties. If the initial array is too small, it will have been doubled and the elements copied multiple times, resulting in slower execution. After a resize, the array is only half full, resulting in wasted space. Even on average, the array will only be three-quarters full.

18.3.2. Linked lists

As we’ve seen, while dynamic arrays can grow to accommodate a large number of items, the performance penalties of repeated copying and the space wasted by unoccupied array elements can negatively affect program behavior. In this section, we introduce the linked list, an alternative data structure that can efficiently grow to accommodate a large number of objects. As we shall see, this efficient growth comes at the expense of limitations on how the structure can be accessed.

Consider again the problem of reading an arbitrary number of names from an input file and storing them. Since we don’t know in advance how many names there are, it might not be efficient to pre-allocate or dynamically grow an array to store them. Instead, imagine that we could write each name on a small index card, and then link the index cards together to keep track of them, much like the cars of a railroad train are linked by the coupling from one to the next.

Constructing a linked list

In Java, a linked list is usually implemented as a class that provides methods to interact with a sequence of objects. The objects in the list are implemented as a private static nested class. A private static nested class behaves like a normal class but can only be created and accessed by the class surrounding it. In this way, the internal representation of the list is hidden and protected from outside modification. The nested class has two fields, one containing the data to be stored and the other containing a link or reference to the next object, or node, in the list. Since they’re only accessed by the outer class, it’s reasonable to make these fields public. If you need a refresher on static nested classes, refer to Section 9.4.

public class LinkedList {
    private static class Node {
        public String value;
        public Node next;
    }

	// Methods for interacting with the list
}

Note that the type next is the same as the class it’s inside of! This apparent circular reference works because the variable only references an object, but the object is not actually contained within the variable. In fact, the value of the link may be null, indicating that there are no additional nodes in the list.

In the railroad metaphor, the node is a train car (with its freight as the value), and the link to the next node is the coupling to the next car.

The definition of LinkedList given above is a good start, but it needs a head reference that keeps track of the first node in the list. Initially, this value is null. We also need an add() method so that we can add nodes to the list. Without checking through the entire list, it’s useful to know how many nodes are in it. We can create a size field that we increment whenever we add a node, as well as an accessor to read its value. Finally, we can create a fillArray() method that fills an array with the values in the list.

Program 18.5 Basic implementation of a linked list class to hold String objects.
public class LinkedList {
    private static class Node {
        public String value;
        public Node next;
    }
 
    private Node head = null;
    private int size = 0;
    
    public void add(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;
        size++;
    }
    
    public int size() {
        return size;
    }
    
    public void fillArray(String[] array) {     
        Node temp = head;
        int position = 0;
        while(temp != null) {
            array[position++] = temp.value;
            temp = temp.next;
        }           
    }
}

Program 18.6 is a re-implementation of the name-reading program using class LinkedList. Note that no array needs to be pre-allocated. Instead, we capture all lines of input in a linked list called list.

Program 18.6 Uses the LinkedList class to store input read from a file.
import java.util.Arrays;
import java.util.Scanner;

public class UseLinkedList {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        LinkedList list = new LinkedList();

        while(in.hasNextLine())
            list.add(in.nextLine());
        
        String[] names = new String[list.size()]; 
        list.fillArray(names);

        Arrays.sort(names);
        
        for(String name: names)
            System.out.println(names);
    }
}

Each time we read a new line from the file, the LinkedList class internally creates a new Node with the input line as its value. It also sets its next reference to the current head so that the rest of the list (which could be empty if head is null) comes after the new Node. We then update the head field to reference the new Node. Thus, each new line read from the file is stored at the beginning of the linked list. The last node in the list, which contains the first String read in, has a next value of null.

Figure 18.4 shows a visualization of the contents of this implementation of a linked list. An “X” is used in place of an arrow that points to null.

linkedlistclasses
Figure 18.4 Visualization of linked list implementation with classes.

Since we also increment the size field inside of LinkedList on each add, we know how many String objects it contains. Thus, we know how large of an array to allocate in Program 18.6. The fillArray() method visits every node in the linked list, storing its value into the allocated array. array. We sort the returned array and then print it as before.

Appending to the end of a linked list

The LinkedList class maintains a field named head that references the first node in the linked list. As we saw, that element was actually the last or most recent String read from input. This head element was followed by the next most recent String, followed by the next most recent String, and so on. The last node contained the first String read from input and had a null next field.

If we want the linked list to be ordered in the natural way, with head pointing to the first element read from the file and the last element in the list (the one with next pointing to null) containing the String most recently read, we can maintain a second field that references the tail of the list.

Program 18.7 adds a tail pointer called tail to the LinkedList class. Note that we have changed the add() method to the addFirst() method, and we have also added an addLast() method to make it easy to append elements to the end of a linked list.

Program 18.7 We can append to the end of a linked list by using an additional variable, tail, to reference the last element (tail) of the list.
public class LinkedListWithTail {
    private static class Node {
        public String value;
        public Node next;
    }
 
    private Node head = null;
    private Node tail = null;
    private int size = 0;
    
    public void addFirst(String value) { (1)
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;
        if(tail == null)
            tail = head;
        size++;
    }
    
    public void addLast(String value) { (2)
        Node temp = new Node();
        temp.value = value;        
        if(tail == null)
            head = temp;
        else 
            tail.next = temp;
		tail = temp;
        size++;     
    }
    
    public int size() {
        return size;
    }
    
    public void fillArray(String[] array) {     
        Node temp = head;
        int position = 0;
        while(temp != null) {
            array[position++] = temp.value;
            temp = temp.next;
        }           
    }
}
1 The addFirst() method has been updated to change the tail pointer, but only if the list is empty (when head is null). After all, adding to the front of a list only changes tail if the front is also the back.
2 In the addLast() method, adding a value to an empty list also sets the head to point at the new node. Once the list has a node in it, subsequent calls to addLast() will point the next field of the old tail at this new node, linking the earlier nodes in the list to the new node at the end.
Inserting into a linked list

In the running example for this chapter, we’re interested in printing a sorted list of String objects read from input. Thus far we’ve captured the lines into a linked list of elements, dumped these elements into an array of the right size, and then sorted the array. An alternative solution is to insert the elements into the linked list at the right point in the first place.

Program 18.8 is a version of a linked list that inserts elements into the linked list in sorted order. The only significant difference between it and the previous implementations of a linked list is its add() method. This method walks down the linked list, starting at head, until it either walks off the end of the list or finds an element before which the new String should go. There are special cases that must be handled to make this process work correctly: empty list, inserting at the beginning, inserting in the middle, and inserting at the end.

Program 18.8 Linked list class in which calling the add() method inserts each value in sorted order.
public class SortedLinkedList {
    private static class Node {
        public String value;
        public Node next;
    }
 
    private Node head = null;
    private Node tail = null;
    private int size = 0;
    
    public void add(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = null;
        
        if(head == null) // Empty list (1)
            head = tail = temp;
        else if(value.compareTo(head.value) < 0) { // Insert at beginning (2)
            temp.next = head;
            head = temp;            
        }
        else { // Insert at middle or end (3)
            Node previous = head;
            Node current = head.next;
            
            while(current != null && value.compareTo(current.value) >= 0) {
                previous = current;
                current = current.next;
            }
            
            previous.next = temp;
            temp.next = current;
            
            if(current == null) // Inserting at end of list (4)
                tail = temp;
        }
        size++;
    }
1 When adding to an empty linked list, the head and tail fields must be set to reference the new node. The next field of the new node is null by default.
2 If inserting at the beginning of a non-empty list, the head must be updated to point to the new node. The next field of the new node is set to the old value of head.
3 Inserting into the middle or the end of a linked list are similar operations. To insert a node into the middle, it’s common to maintain two variables to reference the current and previous nodes while walking down the list. Once the proper insertion point is found (between the previous and current nodes), the next field for the previous node is adjusted to reference the new node, and next field for the new node is set to current.
4 Inserting at the end of a linked list is the same as inserting into the middle, except that the tail field must also be updated to reference the new node.
    public int size() {
        return size;
    }
    
    public void fillArray(String[] array) {     
        Node temp = head;
        int position = 0;
        while(temp != null) {
            array[position++] = temp.value;
            temp = temp.next;
        }           
    }
}

18.4. Syntax: Abstract data types (ADT)

We’ve seen two examples so far of dynamic data structures: dynamic arrays and linked lists. A great deal of complexity can go on inside these data structures, but code that uses these data structures doesn’t need to be aware of the details of the internal implementation. Ideally, user programs could use any data structure that provides the required set of operations.

Our dynamic array and linked list classes were simple examples of abstract data types (ADT). We can design many data structures that hide the details of their implementation inside a class. The user of each class is aware of the operations (public methods) that can be performed on objects of the class but not of the intricacies used to implement those operations. Defining an ADT without regard to an implementation keeps users of the ADT from becoming dependent on details of any particular implementation. It gives maximum freedom to the programmer to choose (and change) the implementation as appropriate for the overall system design.

We generalize a data structure by observing which operations are applied to it. Then, we create an abstraction that formalizes these observations. The idea is to cleanly separate the use and behavior of the data structure from the way in which it’s implemented.

Interfaces are the obvious tool for defining the behavior of a class in Java without specifying its implementation. When defining an ADT in Java, its set of operations becomes the set of methods specified by the interface. Then, any class that implements the ADT must implement the corresponding interface.

In subsequent sections we look at two fundamental abstract data types, stacks and queues, and sample classes that implement them.

18.4.1. Stacks

We’ve already used stacks to solve problems in Chapter 9. Recall that a stack data structure behaves like a stack of books on your desk. When you place a book on the stack, it covers the books that are already there. When you take a book off the stack, you remove the book most recently placed there, exposing the one beneath it.

You can find a simple implementation of a stack in the solution to the infix conversion problem in Section 18.6, but we now examine the stack more deeply as an archetypal ADT. A stack’s restricted set of operations (pushing and popping) is adequate for many tasks and can be implemented in a number of different ways, some more efficient than others.

The acronym FILO (first in, last out) is sometimes used to describe a stack. The last item that’s been pushed onto the stack is the first item to be popped off the stack. In the next section, we’ll study the queue, which is a FIFO (first in, first out) data structure.

18.4.2. Abstract Data Type: Operations on a stack

There are two essential operations on a stack abstract data type, corresponding to placing a book on the pile and removing it: push() and pop(). We also define two additional operations, top() and isEmpty().

  • push(x)
    Push value x onto the stack.

  • pop()
    Pop the object off the top of the stack and return its value.

  • top()
    Return the value from the top of the stack but do not remove it.

  • isEmpty()
    Return true if the stack is empty and false otherwise.

Because a stack’s an abstract data type, we’re not specifically concerned with how these operations are implemented, merely that they are. Thus, we can specify an interface called Stack that requires these four methods.

Program 18.9 Interface specifying the stack ADT.
public interface Stack {
    void push(String value);
    String pop();
    String top();
    boolean isEmpty();
}
Linked list implementation

All the operations defined by the stack ADT (and interface) are implemented as methods in the class LinkedListStack, shown in Program 18.10.

Program 18.10 Class to implement a stack ADT using a linked list.
public class LinkedListStack implements Stack {
    private static class Node {
        public String value;
        public Node next;
    }
    
    private Node head = null; (1)

    public void push(String value) { (2)
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;
    }

    public String pop() { (3)
        String value = null;
        if(isEmpty())
            System.out.println("Can't pop empty stack!");
        else {
            value = head.value;
            head = head.next;
        }
        return value;
    }

    public String top() {
        String value = null;
        if(isEmpty())
            System.out.println("No top on an empty stack!");
        else
            value = head.value;
        return value;
    }

    public boolean isEmpty() {
        return head == null;
    }
}
1 The head field is used to maintain a reference to the linked list that defines the stack. It is initialized to null.
2 The method push() must create a new node for the linked list and push it onto the front of the list. It does so by creating a new Node, setting its value field to the incoming value, and pointing its next pointer to the beginning of the list, stored by head. Since temp is now the new top of the stack, head is made to point at it.
3 The pop() method needs to return the value of the head node and remove that node from the linked list. It does this by replacing the head node with the node pointed at by the next link in head. The pop() method from the simpler stack used in the solution to the nested expressions problem in Section 9.5 merely removed the top and didn’t return the value. Most real-world stack implementations of pop() do return this value, giving programmers more flexibility.

Note that both pop() and top() print an error message if the stack is empty. More elaborate error handling is possible by throwing an exception.

Dynamic array implementation

Like the dynamic array example of Program 18.3, Program 18.11 implements a stack of String values using a dynamic array data structure.

Program 18.11 Illustrates a stack ADT partially implemented using a dynamic array.
import java.util.Arrays;

public class DynamicArrayStack implements Stack {
    private String[] strings = new String[10];
    private int size = 0;
    
    public void push(String string) {
        if(size == strings.length)
            doubleArray();
        strings[size++] = string;
    }
    
    public String pop() {
        String value = null;
        if(size == 0)
            System.out.println("Can't pop empty stack!");
        else
            value = strings[--size];
        return value;
    }
	
	public String top() {
        String value = null;
        if(isEmpty())
            System.out.println("No top on an empty stack!");
        else
            value = strings[size - 1];
        return value;
    }
	
	public boolean isEmpty() {
		return size == 0;
	}

    private void doubleArray() {
        strings = Arrays.copyOfRange(strings, 0, strings.length*2);
    }
}

This stack implementation using a dynamic array omits the top() and isEmpty() methods, causing a compiler error until the Stack interface is fully implemented.

Example 18.1 Postfix computation

At the beginning of the chapter, we introduced the problem of converting an expression from infix to postfix notation. In Section 18.6, we give the solution to this problem, but without a program that can evaluate a postfix expression, the conversion tool isn’t very useful.

Here we give a simple postfix evaluator. Recall the algorithm: Scan the input expression from left to right, if you see a number, put it on the stack. If you see an operator, pop the last two operands off the stack and use the operator on them. Then, push the result back on the stack. When you run out of input, the value at the top of the stack is your answer.

Like the infix to postfix converter, we restrict our input to positive integers of a single digit. To make this program simpler, we introduce two new classes that will be useful in our infix to postfix converter. The first is Term.

public class Term {
    private int value;
    public Term(int value) { this.value = value; }
    public int getValue() { return value; }
}

This class allows us to hold an int value. Although its structure is simple, we update the definition of Term later in the solution to the infix to postfix conversion problem. By doing so, we can keep exactly the same definition for TermStack given next.

Program 18.12 Class to manage a stack of Term objects.
public class TermStack {
    private static class Node {
        public Term value;
        public Node next;
    }
    
    private Node head = null;

    public void push(Term value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;
    }

    public Term pop() {
        Term value = null;
        if (isEmpty())
            System.out.println("Can't pop empty stack!");
        else {
            value = head.value;
            head = head.next;
        }
        return value;
    }

    public Term top() {
        Term value = null;
        if (isEmpty())
            System.out.println("No top on an empty stack!");
        else
            value = head.value;
        return value;
    }

    public boolean isEmpty() {
        return head == null;
    }
}

This class gives a linked list implementation of a stack. In fact, it’s virtually identical to Program 18.10 with the substitution of Term for String.

With our utility classes in place, the code for the postfix evaluator is short.

Program 18.13 Evaluates a postfix expression.
import java.util.*;

public class PostfixEvaluator {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String expression = in.nextLine(); 				(1)
        TermStack stack = new TermStack();
        for(int i = 0; i < expression.length(); i++) {	(2)
            char term = expression.charAt(i); 	
            if(term >= '0'&& term <= '9')    			(3)
                stack.push(new Term(term - '0'));
            else {
                int b = stack.pop().getValue();             
                int a = stack.pop().getValue();             
                switch(term) {							(4)
                    case '+': stack.push(new Term(a + b));
                        break;
                    case '-': stack.push(new Term(a - b));
                        break;
                    case '*': stack.push(new Term(a * b));
                        break;
                    case '/': stack.push(new Term(a / b));
                        break;
                }
            }
        }
        System.out.println("The answer is: " + stack.top().getValue()); (5)
    }
}
1 Our main() method reads in the expression from the user and creates a TermStack called stack.
2 Then, it iterates through the expression with a for loop.
3 For each number we find, we supply it as an argument to the constructor of a new Term object, which we push onto stack.
4 For each operator, we pop two items off stack and apply the operator to them. We create a new Term from the result and push this value onto stack.
5 Finally, after all input is exhausted, we print the value on the top of stack.

To test this program properly, you must supply expressions in postfix form. Also, remember that these operations are all integer operations without fractional parts. Be careful to avoid division by zero!

18.4.3. Queues

A queue data structure is similar to a stack data structure, except that when getting an item from a queue, the item that’s been in the queue longest is the one retrieved. A queue data structure models an ordinary queue or line of people. The first person in line at a bank, for example, is the first one to receive service. Late comers are served in the order in which they arrive.

A queue is sometimes called a FIFO (first in, first out) data structure due to this property. To distinguish the operations on a queue from those on a stack, we use the terms enqueue and dequeue instead of push and pop.

18.4.4. Abstract Data Type: Operations on a queue

Four typical operations on a queue data structure are the following.

  • enqueue(x)
    Put value x at the end of the queue.

  • dequeue()
    Remove and return the value at the front of the queue, that is, the value that’s waiting the longest.

  • front()
    Return the value at the front of the queue but do not remove it.

  • isEmpty()
    Return true if the queue is empty and false otherwise.

As with stacks, we can specify an interface called Queue that requires these four methods.

Program 18.14 Interface specifying the queue ADT.
public interface Queue {
    void enqueue(String value);
    String dequeue();
    String front();
    boolean isEmpty();
}
Linked list implementation

Program 18.15 shows an implementation of the queue ADT operations using a linked list. Because we need to keep track of nodes at both ends of the linked list, we maintain head and tail variables to reference these nodes. The enqueue() and dequeue() methods manipulate these variables to manage the queue as values are put onto it and removed from it.

Program 18.15 Illustrates a queue ADT implemented using a linked list.
public class LinkedListQueue implements Queue {
    private static class Node {
        public String value;
        public Node next;
    }
    
    private Node head = null;
    private Node tail = null;    
    
    public void enqueue(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = null;        
        if(isEmpty())
            head = temp;
        else          
            tail.next = temp;
		tail = temp;            
    }
    
    public String dequeue() { 
        String value = null;
        if(isEmpty())
            System.out.println("Can't dequeue an empty queue!");
        else {
            value = head.value;
            head = head.next;
            if(head == null)
                tail = null;
        }
        return value;
    }
    
    public String front() {
        String value = null;
        if(isEmpty())
            System.out.println("No front on an empty queue!");
        else
            value = head.value;
        return value;
    }
    
    public boolean isEmpty() {
        return head == null;
    }
}

Note that the implementation of the LinkedListQueue class is very similar to the implementation of the LinkedListWithTail class. The enqueue() method in the former is almost identical to the addLast() method in the latter.

18.5. Advanced: Generic data structures

Most of the dynamic data structures we’ve seen in this chapter store values of type String. We’ve explored dynamic arrays of String values, linked lists of String objects, queues of String objects, and stacks of String objects. In Example 18.1, we created the stack class TermStack to hold Term objects, but TermStack is identical to the existing LinkedListStack class with the substitution of Term for String.

What if you wanted to store values of some other type in these data structures? What if you wanted a stack of int values or a queue of Thread objects? You might think that you need to create a distinct but similar implementation of each ADT for each type, as we do in Example 18.1.

One possible solution is to take advantage of the fact that a variable of type Object can hold a reference to a value of any reference type, since all classes are subtypes of Object. If we create data structures using Object as the underlying type, we can store values of any type in the data structure. For example, Program 18.16 is an implementation of a stack ADT with an underlying data type of Object.

Program 18.16 Class that implements a stack of Object references.
public class ObjectStack {
    private static class Node {
        public Object value;
        public Node next;
    }
    
    private Node head = null;

    public void push(Object value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;
    }

    public Object pop() {
        Object value = null;
        if(isEmpty())
            System.out.println("Can't pop empty stack!");
        else {
            value = head.value;
            head = head.next;
        }
        return value;
    }

    public Object top() {
        Object value = null;
        if(isEmpty())
            System.out.println("Can't get top of empty stack!");
        else
            value = head.value;
        return value;
    }

    public boolean isEmpty() {
        return head == null;
    }
}

A stack of Object references could be an example of a heterogeneous data structure since it’s possible to push objects of different types onto the same stack. While there are situations in which this technique is useful, in most cases a homogeneous data structure (where all values are of the same type) is all that’s needed. Homogeneous data structures allow type checking to occur at compile time, thus helping to avoid run-time errors.

Using a stack of Object references is generally more cumbersome, since you must cast values returned from pop() or top() to the appropriate data type.

ObjectStack stack = new ObjectStack();
stack.push("hello");
String result = (String)stack.pop(); (1)
1 Without the explicit cast to String, the compiler gives an error: Type mismatch: cannot convert from Object to String.

Casting the returning value from a heterogeneous data structure essentially forces type checking to move from compile time to run time. Instead of having the Java compiler verify the type correctness of operations, we force the Java virtual machine to do the check.

18.5.1. Generics in Java

Java provides a general facility to create classes that implement the same basic ADT but with a different underlying data type. This mechanism preserves the advantages of compile-time type checking and eliminates the need for run-time casting. A generic class is a class that gives a template for creating classes in which a placeholder for an underlying data type can be filled in when a specific instance of that class is created. In the case of Example 18.1, we need a stack that can hold Term objects instead of String objects, and a generic class would allow us to create a stack of any reference type.

The generics facility in Java only supports underlying data types that are reference types like String and other classes, not primitive types like int or boolean. However, we can use wrapper classes to hold primitives types. Thus, a generic stack of int values needs to be implemented as a stack of Integer objects. Fortunately, Java automatically converts between int and Integer in most cases.

Defining a simple generic class in Java is done by appending a type parameter within angle brackets (<>) to the end of the class name being defined.

public class GenericClass<T> {
    ...
    T transform(T item) {
        ...
    }
}

This code defines a new generic class (think class template) GenericClass with underlying type T. It includes a method transform() that takes a value of type T and transforms it (in some unspecified way) to another value of type T.

To use a generic class properly, you must create instances of it specifying the underlying type. Then, the compiler will check using the appropriate type at compile time. The compiler must make sure that all the operations are valid with the supplied type substituted for the type parameter (T in this example).

For example, to create and use an instance of GenericClass with underlying type String, you might do the following.

GenericClass<String> genericString = new GenericClass<String>();
String output = generic.transform("hello");

Because this use of the GenericClass class is defined for underlying type String, no casting is necessary to assign the result of the transform() method to the String variable output.

To create and use an instance of GenericClass with underlying type Integer, you would type:

GenericClass<Integer> genericInteger = new GenericClass<Integer>();
int i = generic.transform(27);

The same definition of GenericClass is used in both instances with different underlying data types, and the compiler is able to verify at compile time that the uses are type safe.

If you omit the underlying type when declaring a generic variable or creating an instance of a generic type, the compiler uses Object as the underlying type. This use, called a raw type, is essentially like not using generics at all. There’s no compile-time type checking, and references must be cast as needed. Modern Java compilers generally issue a warning when raw types are used.

GenericClass genericRaw = new GenericClass(); // Raw type
int i = (Integer)genericRaw.transform(27); // Cast needed

The next two examples illustrate defining generic classes in Java.

Example 18.2 Defining a generic linked list

Program 18.17 defines a generic version of the LinkedList class shown earlier. Note that it’s necessary to include the type parameter T on the outer class as well as the nested class Node.

Program 18.17 Class that implements a generic linked list.
public class GenericLinkedList<T> {
    private static class Node<T> {
        public T value;
        public Node<T> next;
    }
 
    private Node<T> head = null;
    private int size = 0;
    
    public void add(T value) {
        Node<T> temp = new Node<T>();
        temp.value = value;
        temp.next = head;
        head = temp;
        size++;
    }
    
    public int size() {
        return size;
    }
    
    public void fillArray(T[] array) {      
        Node<T> temp = head;
        int position = 0;
        while(temp != null) {
            array[position++] = temp.value;
            temp = temp.next;
        }           
    }
}

This class is almost indistinguishable from Program 18.5 except that is uses type T instead of String.

Using generics is mostly like using any other class, but there are a few oddities. In particular, there are problems instantiating arrays with generic types. The fillArray() method works because it never creates the array, only fills it.

18.5.2. Using a Generic Class

Creating an instance of a generic class is similar to creating an instance of a regular class, except that you should specify the missing type (or types) used to parameterize the generic class. For example, if you want to create an instance of the GenericClass<T> class, you must specify the type T, for example new GenericClass<String>().

Program 18.18 uses the generic class GenericLinkedList parameterized by String to re-implement Program 18.6.

Program 18.18 Uses the generic class GenericLinkedList to create and use a linked list of Strings.
import java.util.Arrays;
import java.util.Scanner;

public class UseGenericLinkedList {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        GenericLinkedList<String> list = new GenericLinkedList<String>();
        
        while(in.hasNextLine())
            list.add(in.nextLine());
        
        String[] names = new String[list.size()]; 
        list.fillArray(names);
        
        Arrays.sort(names);
        
        for(String name: names)
            System.out.println(name);
    }
}

Java 7 and later allow generic type inference. Type inference means that the compiler is able to guess what type you mean without explicitly typing it. Type inference is intended as a convenience so that you don’t have to type as much repetitive code. The most common form of generic type inference uses the diamond operator (<>). This operator is used when instantiating a generic type to show the compiler that you want to use a generic type parameter that you believe the compiler can determine for itself.

For example, the following line appears above without type inference.

GenericLinkedList<String> list = new GenericLinkedList<String>();

Using type inference, it can be shortened to the following.

GenericLinkedList<String> list = new GenericLinkedList<>();

In general, you must always type the full generic type parameter when declaring a variable, but you can often use the diamond operator when instantiating a new generic object.

18.5.3. Using Java Libraries

Many Java library classes use generics to make them more flexible. The java.util package includes many classes to implement stacks, queues, dynamic arrays, sets, and other useful data structures. These container classes are parameterized so that they can be created to hold many different types. We illustrate three examples here: Vector, ArrayList, and HashMap. Note that there’s also a LinkedList class, which is a great deal more powerful than the LinkedList class defined in this chapter. Any class that implements the Iterable interface can also be used in the enhanced for loops described in Section 6.9.1. The ArrayDeque, ArrayList, HashSet, TreeSet, and Vector classes all implement Iterable. In our examples, a Vector object and a Set (returned by the entrySet() method of a HashMap) are used as targets of enhanced for loops.

Example 18.3 Vectors

A Vector (java.util.Vector) implements an array of objects that can grow at run time. The array is automatically extended whenever an attempt is made to store an item exactly one location beyond the last element. Unlike a linked list, Vector elements can be efficiently accessed in any order (by specifying the index, just like an ordinary array). An element can be inserted into the middle of the Vector, causing any elements after the insertion point to be pushed back by an index. Arbitrary elements can also be deleted from the Vector using the remove() method.

Program 18.19 illustrates a use of the Vector class. The program creates an empty Vector and generates random integers between 1 and 10, appending them to the end of the vector, until their sum is at least 100. Then, it prints the integers, their sum, and how many were generated.

Program 18.19 Illustrates the use of the Vector class.
import java.util.Random;
import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        Random random = new Random();        
        Vector<Integer> vector = new Vector<Integer>();
        
        int sum = 0;
        while(sum < 100) {
            int n = random.nextInt(10) + 1;
            vector.add(n);  // append n to end of vector
            sum += n;
        }

        for(int n: vector)
            System.out.format("%3d%n", n);
        System.out.println("---");
        System.out.format("%3d (%d values)%n", sum, vector.size());
    }
}

Output from a typical run of Program 18.19 is shown below:

  9
  9
  8
  7
  7
  4
  7
  6
  8
  7
  9
  4
  9
 10
---
104 (14 values)
Example 18.4 Maps

The HashMap(java.util.HashMap) is a very useful, general-purpose data structure that maintains a dictionary of entries. A dictionary associates unique keys with values. You can think of it as mapping a key to a value. In the Java HashMap class, keys and values can be arbitrary Java classes.

Program 18.20 reads a sequence of lines containing names and ages. For simplicity, each name is one word, and each age is a simple integer. It stores these (name, age) pairs in a HashMap<String,Integer> data structure. Once all the input is read and in.hasNext() returns false, the program prints all the keys (names), then all the values (ages), and finally it prints the names and ages of each person in the input file.

Program 18.20 Illustrates using a HashMap dictionary to store a mapping from names to ages.
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String,Integer> map = new HashMap<>();
        
        Scanner in = new Scanner(System.in);
        while(in.hasNext()) {
            String name = in.next();
            int age = in.nextInt();
            map.put(name, age);
        }
    
        System.out.println("Keys");
        for(String name: map.keySet())
            System.out.println("\t" + name);
        
        System.out.println("Values");
        for(int age: map.values())
            System.out.println("\t" + age);
        
        for(Map.Entry<String, Integer> entry: map.entrySet())
			System.out.println(entry.getKey() + " -> " + entry.getValue());
    }
}

Shown below is the output for a simple input file.

Keys
	Kathy
	Martha
	Fred
	Henway
	Michael
	Henry
	John
	Margarette
	Edward
	Tim
	Hamcost
Values
	60
	22
	15
	1
	21
	31
	23
	57
	12
	57
	2
Kathy -> 60
Martha -> 22
Fred -> 15
Henway -> 1
Michael -> 21
Henry -> 31
John -> 23
Margarette -> 57
Edward -> 12
Tim -> 57
Hamcost -> 2

18.6. Solution: Infix conversion

Here we give our solution to the infix conversion problem from the beginning of the chapter. As in Example 18.1, we use a stack of Term objects to solve the problem. However, we expand the Term class to hold both operands and operators. We only add methods and fields to the earlier definition, taking nothing away. In this way, we should be able to use the Term class for both infix to postfix conversion and postfix calculation.

public class Term { 
    private int value;  
    private char operator;
    private boolean isOperator;

Here we’ve augmented the earlier Term class by adding two more fields, a char called operator to hold an operator and a boolean called isOperator to keep track of whether or not our Term object holds an operator or an operand.

    public Term(int value) {
        this.value = value;
        isOperator = false;
    }
    
    public Term(char operator) {
        this.operator = operator;
        isOperator = true;
    }

We now have two constructors. The first one takes an int value and stores it into value, setting isOperator to false to indicate that the Term object must be an operand. The second constructor takes a char value and stores it into operator, setting isOperator to true to indicate that the Term object must be an operator (such as +, -,*, or /).

    public int getValue() { return value; } 
    public char getOperator() { return operator; }          
    public boolean isOperator() { return isOperator; }  

These three accessors give back the operand value, the operator character, and whether or not the object is an operator, respectively. This solution is not necessarily the most elegant from an OOP perspective. The code that uses a Term object needs to choose the getOperator() method or the getValue() method depending on whether the Term is an operator. This design opens up the possibility that some code will call the wrong accessor method and get a useless default value.

    public boolean greaterOrEqual(Term term) {
        if(isOperator())
            switch(operator) {            
                case '*':
                case '/': return true;              
                case '+':
                case '-': return term.operator != '*' && term.operator != '/';
                default: return false;
            }       
        else
            return false;
    }
}

The most complicated addition to the Term class is the greaterOrEqual() method, which takes in another Term object. This method compares the operator of the Term object being called with the one that’s being passed in as a parameter. Because this method is in the Term class, it can access the private variables of the term parameter. This method returns true if the operator of the called object has a greater or equal precedence compared to the operator of the parameter object. The meat of the method is the switch statement that establishes the high precedence of * and /, the medium precedence of + and -, and the low precedence of anything else, namely the left parenthesis (.

With this updated Term class, we can create Term objects that hold either an operator or an operand and allow the precedence of operators to be compared. We use exactly the same TermStack class from Example 18.1 for our stack. All that remains is the client code that parses the input.

import java.util.*;

public class InfixToPostfix {
    public static void main(String[] args) {        
        Scanner in = new Scanner(System.in);
        String expression = in.nextLine(); 	(1)
        TermStack stack = new TermStack();	(2)
        String postfix = "";        		(3)
1 We read in the input expression.
2 We create a TermStack called stack to aid in conversion.
3 We also declare an empty String called postfix to hold the output.
        for(int i = 0; i < expression.length(); i++) {	(1)
            char term = expression.charAt(i);
            if(term >= '0' && term <= '9')				(2)
                postfix += term;        
            else if(term == '(')						(3)
                    stack.push(new Term(term));
            else if(term == ')') {						(4)
                while(stack.top().getOperator() != '(') {
                    postfix += stack.top().getOperator();
                    stack.pop();
                }
                stack.pop(); // Pop off the '('
            }
            else if(term == '*' || term == '/' || term == '+' || term == '-') {
                Term operator = new Term(term);			(5)
                while(!stack.isEmpty() && stack.top().greaterOrEqual(operator)) {
                    postfix += stack.top().getOperator();
                    stack.pop();
                }
                stack.push(operator);
            }                   
        }
1 This for loop runs through each char in the input expression and applies the four rules given in the description of the infix conversion problem.
2 If a term is an operand, it’s added directly to the output.
3 If a term is a left parenthesis, it’s pushed onto the stack.
4 If a term is a right parenthesis, all the terms on the stack are popped off and added to the output until a left parenthesis is reached.
5 If a term is a normal operator, the top of the stack is repeatedly popped and added to output as long as it has a precedence greater than or equal to the new operator. The complexity of doing this precedence comparison is tucked away inside of the Term class.
        while(!stack.isEmpty()) { 		(1)
            postfix += stack.top().getOperator();
            stack.pop();
        }       
        System.out.println(postfix);	(2)
    }
}
1 After the input has all been consumed, we pop any remaining operators off the stack and add them to the output.
2 Finally, we print the output.

The output from this program could be used as the input to the postfix evaluator program from Example 18.1. A more complex program that did both the conversion and the calculation might want to store everything in a queue of Term objects instead of producing String output and then recreating Term objects.

18.7. Concurrency: Linked lists and thread safety

The implementations of stacks and queues in the previous sections are not thread-safe. If multiple threads use a stack or queue object simultaneously, the head or tail pointers can become inconsistent or be updated incorrectly, potentially causing the stack or queue to lose elements. As you’ve seen, multiple threads operating on the same data can produce unexpected results.

Program 18.21 is a simple multi-threaded program to test (and break!) the thread safety of the queue implementation in Program 18.15.

Program 18.21 Tests the queue implementation, including its thread safety.
public class UseLinkedListQueue extends Thread {
    private static final int THREADS = 10;
    private LinkedListQueue queue;
    private boolean adding;
    
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS];
		LinkedListQueue queue = new LinkedListQueue(); (1)
		
        for(int i = 0; i < THREADS; i++) {
            threads[i] = new UseLinkedListQueue(queue, true);
            threads[i].start();	(2)
        }
        
        for(int i = 0; i < THREADS; i++)
			threads[i].join(); 	(3)
                    
        for(int i = 0; i < THREADS; i++) {
            threads[i] = new UseLinkedListQueue(queue, false);
            threads[i].start();	(4)
        }
 
        for(int i = 0; i < THREADS; i++)
            threads[i].join();
        
        while(!queue.isEmpty())
            System.out.println("Left in queue: ID = " + queue.dequeue());
    }
1 The program creates a queue.
2 It then creates and starts 10 threads, passing them the queue and true for their adding value.
3 Then, the program joins the threads until each has finished.
4 The program starts 10 more threads, passing them the same queue but false for their adding value.
	public UseLinkedListQueue(LinkedListQueue queue, boolean adding) { (1)
		this.queue = queue;
		this.adding = adding;
	}
    
    public void run() {
        if(adding) {
            long ID = Thread.currentThread().getId();
            System.out.println("Thread ID added to queue: " + ID);
            queue.enqueue("" + ID); (2)
        }
        else {
            String ID = queue.dequeue();
            System.out.println("Thread ID removed from queue: " + ID); (3)
        }
    }
}
1 Each thread’s constructor takes a reference to the shared queue and a boolean that specifies whether it’s an adding or a removing thread.
2 Threads in the adding phase add a String version of their thread ID numbers to the queue and print it out.
3 Threads not in the adding phase each remove one value from the queue and print it out.

Without appropriate synchronization, the program may not correctly link all values into the queue nor remove them at the end. A typical error-prone output run is shown here.

Thread ID added to queue: 9
Thread ID added to queue: 14
Thread ID added to queue: 13
Thread ID added to queue: 12
Thread ID added to queue: 11
Thread ID added to queue: 10
Thread ID added to queue: 18
Thread ID added to queue: 17
Thread ID added to queue: 16
Thread ID added to queue: 15
Thread ID removed from queue: 14
Thread ID removed from queue: 11
Thread ID removed from queue: 12
Thread ID removed from queue: 16
Thread ID removed from queue: 17
Thread ID removed from queue: 18
Thread ID removed from queue: 10
Can't dequeue an empty queue!
Can't dequeue an empty queue!
Thread ID removed from queue: 15
Thread ID removed from queue: null
Thread ID removed from queue: null

How does this implementation fail? Consider the situation in which two threads are attempting to put a value in the queue simultaneously by calling the enqueue() method in Program 18.15. Suppose the first thread tests the queue and finds it empty (isEmpty() returns true) but is then interrupted. If a second thread gets control, it will also see that the queue’s empty, set the head and tail variables to the new Node object temp, and return. The first thread will eventually wake up, still thinking that the queue is empty, and also set the head and tail variables to its own new Node temp. But these assignments overwrite the assignments just done by the previous thread! The initial node that was in the queue is now lost. Note that there are other sequences of execution that can cause similar race conditions.

This problem can be fixed by ensuring that once one thread starts examining and modifying queue variables, no other thread can access the same variables until the first one is finished. As shown in Chapter 14, this mutual exclusion can be achieved by using the synchronized keyword on methods that need to have exclusive access to object variables. In this queue implementation, we need to synchronize access by threads that are using either the enqueue() or dequeue() methods, since both methods access and manipulate variables in the object. Although it’s not called in this program, the front() method should also be synchronized so that a null head is not accessed accidentally. The isEmpty() method doesn’t need to be synchronized since the only methods that call it that can do any harm are already synchronized. Outside code that calls isEmpty() might get the wrong value if another thread modifies the contents of the queue, but there’s no guarantee that other threads won’t modify the state of the queue at any point after the isEmpty() method is called anyway.

Program 18.22 Synchronized version of the queue class that allows thread-safe use.
public class LinkedListQueueSafe implements Queue {
    private static class Node {
        public String value;
        public Node next;
    }
    
    private Node head = null;
    private Node tail = null;  
    
    public synchronized void enqueue(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = null;        
        if(isEmpty())
            head = temp;
        else          
            tail.next = temp;
		tail = temp; 
    }
    
    public synchronized String dequeue() {
        String value = null;
        if(isEmpty())
            System.out.println("Can't dequeue an empty queue!");
        else {
            value = head.value;
            head = head.next;
            if(head == null)
                tail = null;
        }
        return value;
    }
    
    public synchronized String front() {
        String value = null;
        if(isEmpty())
            System.out.println("No front on an empty queue!");
        else
            value = head.value;
        return value;
    }
    
    public boolean isEmpty() {
        return head == null;
    }
}

With both enqueue() and dequeue() methods synchronized as in Program 18.22, a typical output generated by the program is shown below.

Thread ID added to queue: 9
Thread ID added to queue: 14
Thread ID added to queue: 12
Thread ID added to queue: 13
Thread ID added to queue: 10
Thread ID added to queue: 11
Thread ID added to queue: 18
Thread ID added to queue: 17
Thread ID added to queue: 16
Thread ID added to queue: 15
Thread ID removed from queue: 9
Thread ID removed from queue: 18
Thread ID removed from queue: 13
Thread ID removed from queue: 17
Thread ID removed from queue: 15
Thread ID removed from queue: 16
Thread ID removed from queue: 14
Thread ID removed from queue: 12
Thread ID removed from queue: 10
Thread ID removed from queue: 11

18.8. Concurrency: Thread-safe libraries

As we mentioned in Section 9.6, some libraries are thread safe and some are not. The Java Collections Framework (JCF) is a very useful library, but it’s also a library that requires you to manage your own thread safety if it matters for your program.

The JCF defines the Collection interface and the Map interface. The Collection interface, which any collection of objects should implement, has subinterfaces Set, List, and Queue which define the basic operations in Java that are needed to implement a set, list, or queue of items. The Map interface gives the basic operations for a dictionary, a collection of key-value pairs, one implementation of which is the HashMap from Example 18.4.

As we mentioned in Chapter 10, an interface can’t mark a method with the synchronized keyword. Consequently, the JCF makes no guarantee about the thread safety of a container based on which interface it implements. The programmer must read the documentation carefully in order to know if a container is thread safe and react accordingly.

Example 18.5 ArrayList

An ArrayList is like a Vector, with essentially the same interface but without synchronization. That is, if two threads attempt to insert or remove an element from the same ArrayList at the same time, the internal state of the ArrayList might become corrupt, or the results might be incorrect.

Program 18.23 is an example of synchronizing updates to the ArrayList class with multiple threads.

Program 18.23 Example of thread-safe use of an ArrayList.
import java.util.ArrayList;

public class ArrayListExample extends Thread {
    private ArrayList<String> list;

    public static void main(String[] args) throws InterruptedException {
        ArrayList<String> list = new ArrayList<String>(); (1)
        
        Thread t1 = new ArrayListExample(list); (2)
        Thread t2 = new ArrayListExample(list);
        t1.start(); 
        t2.start();

		t1.join();
		t2.join();
        
        for(String text: list) (3)
            System.out.println(text);
    }
1 The main() method creates an ArrayList.
2 It then creates and starts two threads, passing in the ArrayList as an argument to each so that they both share the list.
3 After waiting for the threads to finish, the main() method prints out the contents of the list.
	public ArrayListExample(ArrayList<String> list) {
		this.list = list; 				(1)
	}
    
    public void run() { 
        for(int i = 0; i < 10; i++) { 	(2)
            synchronized(list) { 		(3)
                list.add(this.getName() + ": " + i);
            }
            try { 
				Thread.sleep(1); 		(4)
			}
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
1 The constructor stores a reference to the shared ArrayList.
2 Each thread repeats a loop 10 times, appending a String to the ArrayList on each iteration.
3 To prevent concurrent updates from happening, each thread synchronizes on the shared variable list.
4 To make concurrent update attempts more likely without synchronization, the thread sleeps for a millisecond on each iteration.

Without the synchronized keyword, a typical run, shown below, includes a null reference in the output, indicating that the internal ArrayList data structure wasn’t updated correctly.

Thread-1: 0
Thread-0: 0
Thread-1: 1
Thread-0: 1
Thread-1: 2
Thread-0: 2
Thread-0: 3
Thread-1: 3
Thread-1: 4
Thread-0: 4
null
Thread-0: 5
Thread-1: 6
Thread-0: 6
Thread-1: 7
Thread-0: 7
Thread-1: 8
Thread-0: 8
Thread-0: 9
Thread-1: 9

With the synchronized keyword, each run includes exactly the same number of entries from each thread, although the threads don’t always alternate in strict lockstep.

Thread-0: 0
Thread-1: 0
Thread-0: 1
Thread-1: 1
Thread-1: 2
Thread-0: 2
Thread-1: 3
Thread-0: 3
Thread-1: 4
Thread-0: 4
Thread-0: 5
Thread-1: 5
Thread-1: 6
Thread-0: 6
Thread-1: 7
Thread-0: 7
Thread-1: 8
Thread-0: 8
Thread-1: 9
Thread-0: 9

ArrayList is much more widely used than Vector, not in spite of its lack synchronization but because of that lack. Synchronization tools have overhead, slowing down execution. Most programmers aren’t focused on writing thread-safe code and prefer faster execution.

Whenever synchronization doesn’t matter, ArrayList is clearly a better choice than Vector. When synchronization does matter, the programmer must decide whether to use Vector or to use ArrayList with explicit synchronization tools.

18.9. Exercises

Conceptual Problems

  1. Explain the difference between static data structures and dynamic data structures.

  2. In which situations is it better to use a dynamic array? In which situations is it better to use a linked list? Explain why in each case.

  3. On which line in Program 18.1 is an exception generated? Why?

  4. In Program 18.2, is it possible to post-increment count inside the try clause rather than at the bottom of the while loop?

  5. Explain why the array inside the names object in Program 18.4 is, on average, only three-quarters full.

  6. Based on the stack implementation in Program 18.10, draw a picture of the linked list structure after each of the following statements.

    LinkedListStack stack = new LinkedListStack();
    stack.push("hello");
    stack.push("goodbye");
    stack.pop();
    stack.push("there");
    stack.push("cruel");
    stack.pop();
    stack.push("world");
  7. Implement the methods top() and isEmpty() for the dynamic array implementation of the stack in Program 18.11.

  8. Based on queue implementation in Program 18.15, draw a picture of the linked list structure after each of the following statements.

    LinkedListStack queue = new LinkedListStack();
    stack.enqueue("hello");
    stack.enqueue("there");
    stack.enqueue("world");
    stack.dequeue();
    stack.enqueue("cruel");
    stack.dequeue();
    stack.enqueue("goodbye");

Programming Practice

  1. Implement a version of DynamicArray from Program 18.3 that shrinks the size of its internal storage array to half its size when only one quarter of its capacity is being used. This design can save significant amounts of space if a large number of items are added to the dynamic array at once and then removed.

  2. Consider Program 18.7 which defines the LinkedListWithTail class for storing a linked list of String values. Add a reverse() method to the class which reverses the order of the nodes in the linked list. The key idea is make a new linked list that holds the head of the list. Then, remove the head from the original linked list. Put the next node in front of the head in the new linked list and remove it from the old. Continue the process until there’s nothing left in the original list. Be sure to reset the head and tail references correctly after the reversal.

  3. In Section 18.2.2, we used two kinds of linked lists to store data but copy all that data back into an array before sorting it. We also used a third linked list class, SortedLinkedList, to insert data and maintain a sorted order. However, it’s possible to add data in non-sorted order to a linked list and then sort it afterward. Add a sort() method to the LinkedListWithTail class that performs a bubble sort on the nodes inside.

    The algorithm for a bubble sort is described in Section 10.1. The idea is to make repeated passes through a list, swapping two adjacent items if they’re out of order. You keep making passes over the list until no adjacent items are out of order. For a this sort() method, you’ll need to use the compareTo() method to compare the String values in the linked list nodes. Also, it might be necessary to have special cases that update the head and tail pointers if those nodes are swapped with other nodes. Note that bubble sort is not the fastest way to sort a linked list. We introduce a faster approach in Chapter 19.

  4. Create JUnit test cases to verify that the synchronized keywords are needed on the set() and sort() methods of the DynamicArray example from Program 18.3. To test the set() method, you can create one thread that repeatedly sets, gets, and tests a changing value at a fixed location (e.g., 0) and another thread that continuously appends to the array (causing it to grow by copy and replace, thus occasionally overwriting the value at the fixed location). To test the sort() method, create two threads that sort the same large random array at the same time. Check to see if the array is, in fact, actually sorted after the threads have exited. For both tests, you might need to repeat the operations a number of times to trigger the race condition.

  5. To make an infix converter that can handle floating-point values or even just integers with more than one digit, you need to make a pass over the input, parsing the sequence of characters into terms. When an expression is in infix notation, the order of terms is an operand followed by an operator, repeated over and over, and finishing on an operand. There are two exceptions: Whenever you’re expecting an operand, you might get a left parenthesis, but after the parenthesis, you’ll continue to look for an operand. Whenever you’re expecting an operator, you might get a right parenthesis, but after that parenthesis, you’ll continue to look for an operator.

    Using this first pass over input to separate terms as well as the Double.parseDouble() method to compute the equivalent double values of operands, rewrite the solution from Section 18.6 to convert your terms into postfix ordering and then calculate the answer.

  6. Re-implement the solution to the infix conversion problem given in Section 18.6 so that it uses GenericStack with a type parameter of Term instead of TermStack.

  7. Interfaces can also be generic. Consider the following generic version of Queue.

    public interface Queue<T> {
    	void enqueue(T value);
    	T dequeue();
    	T front();
    	boolean isEmpty();
    }

    Re-implement LinkedListQueue so that it’s generic with type parameter T and implements interface Queue<T>.

Experiments

  1. Create an ArrayList<Integer> container and add 1,000,000 random integers to it. Then, create a Vector<Integer> container and add another 1,000,000 random integers to it. Time both sequences of adds. How much slower are the adds to the Vector<Integer> container?

19. Recursion

In order to understand recursion, you must first understand recursion.

— Anonymous

19.1. Problem: Maze of doom

The evil mastermind from Chapter 13 has returned with a new attempt at world domination. Since he now knows that you can use concurrency to crack his security code, this time he’s hidden his deadly virus in a secret location protected by a complex maze of walls and passageways. Fortunately, you’ve been able to get a copy of the maze floor plan, but now you must write a program to find a path through it so you can steal the deadly virus before the evil mastermind unleashes it on the world.

maze
Figure 19.1 Example of a maze to solve.

Finding a path through a maze involves systematically exploring twists and turns, keeping track of where you’ve been, backtracking out of dead ends, and producing a resulting path once you make it through. You already have the basic tools necessary to solve this problem. You can, for example, represent the maze with a two-dimensional array of characters, where a plus (''+) represents a wall and a space (' ') represents a passageway. You could mark a path through the maze by replacing a contiguous (vertical and horizontal but not diagonal) sequence of ' ' characters by '*' characters, leading from the starting square to the final square where the deadly virus is located.

The difficulties are dead ends and, worse, loops. How do you keep track of which paths you’ve tried that didn’t work? While you could use additional data structures to store this information, recursion is a solution technique that makes solving problems like this one surprisingly straightforward.

19.2. Concepts: Recursive problem solving

The idea that we’ll use to solve this maze problem is called recursion. Imagine you’re in a maze and have the choice to go right, left, or straight. No matter which of the three paths you take, you’ll probably be confronted by more choices of going right, left, or straight as you progress. You need to explore them all systematically. The process of systematically exploring the right path is similar to the process of systematically exploring the left path. The choice at this moment between left, right, and straight is in fact part of the same systematic process you want to follow when you’re in the left, right, or straight branches of the maze.

Solutions that can be described in terms of themselves are recursive. But what is recursion? How can describing something in terms of itself be useful? Since recursion sounds circular, how can it be applied to problem solving in Java? How does the computer keep track of these self-references? The following subsections address these questions.

19.2.1. What is recursion?

In the context of computer science and mathematics, recursion means describing some repetitive process in terms of itself. Many complex things can be described elegantly using recursion.

Consider the question, “How old are you now?” If you’re 27, you could answer, “I’m one year older than I was last year.” If then asked, “Well, how old were you last year?” Again, you could answer, “I was one year older than I was the year before.” Assuming that the person who wanted to wanted to know your age was very patient, you could repeat this answer over and over, explaining that each year you’ve been one year older than the previous year. However, after being asked 27 times about your age on the previous year, you’d run out of years of life and be forced to answer, “Zero years old.”

This absurd dialog shows an important feature of useful recursive definitions: They have at least one base case and at least one recursive case. The base case is the part of the definition that’s not described in terms of itself. It’s a concrete answer. Without the base case, the process would never end, and the definition would be meaningless. The recursive case is the part of the definition that is defined in terms of itself. Without the recursive case, the definition could only describe a finite set of things.

In the example above, the base case is being zero years old. You have no age before that. The recursive case is any age greater than zero . We can use mathematical notation to describe your age in a given year. Here age(year) is a function that gives the age you were during year.

ageRecursion

To be meaningful, recursive cases should be defined in terms of simpler or smaller values. In English, it’s equally correct to say that you are now a year younger than you’ll be next year. Unfortunately, the age that you’ll be next year is not any closer to a base case, making that recursion useless.

trees
Figure 19.2 Recursive refinement generating a tree-like image.

A recursive definition for your age suggests that recursion is all around us. Though recursive definitions written in formal notation might seem artificial, self-similarity is a constant theme in art and nature. The branching of a trunk of a tree is similar to the branching of its limbs, which is similar to the branching of its branches, which in turn is similar to the branching of its twigs. In fact, we borrow the idea of the branching of a tree to define a recursive data structure in Section 19.4. Figure 19.2 starts with a simple Y-shaped branching. By successively replacing the branches with the previous shape, a tree is generated recursively. Fractals are images generated by similar recursive techniques. Although many real trees exhibit recursive tendencies, they don’t follow rules consistently or rigidly.

19.2.2. Recursive definitions

Although recursion crops up throughout the world, certain forms of recursion are more useful for solving problems. As with many aspects of programming, the recursion we use has a strong connection to mathematical principles.

In mathematics, a recursive definition is one that is defined in terms of itself. It’s common to define functions, sequences, and sets recursively. Functions, such as f(n), are usually defined in relation to the same function with a smaller input, such as f(n - 1). Sequences, such as sn, are usually defined in relation to earlier elements in the sequence, such as sn-1.

Example 19.1 Multiplication defined recursively

Even very common functions can be defined recursively. Consider the multiplication x â‹… y. This multiplication means repeatedly adding x a total of y times. If y is a positive integer, we can describe this multiplication with the following recursive definition. Note the base case and recursive case.

multiplicationRecursion

Multiplication seems like such a basic operation that there would be no need to have such a definition. Yet mathematicians often use multiple equivalent definitions to prove results. Furthermore, this elementary definition provides intuition for creating more complex definitions.

Example 19.2 Factorial defined recursively

Another mathematical function with a natural recursive definition is the factorial function, often written n!. The factorial function is used heavily in probability and counting. The value of n! = n â‹… (n - 1) â‹… (n - 2) â‹… …​ â‹… 3 â‹… 2 â‹… 1. Mathematicians like recursive definitions because they’re able to describe functions and sequences precisely without using ellipses (…​).

factorialRecursion

Note that the base case gives 1 as the answer when n = 0. By convention 0! = 1. Thus, this definition correctly gives 0! = 1, 1! = 1 â‹… 0! = 1, 2! = 2 â‹… 1! = 2, 3! = 3 â‹… 2! = 6, and so on.

Example 19.3 Fibonacci defined recursively

The Fibonacci sequence is an infinite sequence of integers starting with 1, 1, 2, 3, 5, 8, 13, 21, …​. Each term after the two initial 1s is the sum of the previous two terms in the sequence. Fibonacci has many interesting properties and crops up in surprisingly diverse areas of mathematics. It was originally developed to model the growth of rabbit populations.

The Fibonacci sequence also has a natural recursive mathematical definition. Indeed, you may have noticed that we described each term as the sum of the two previous terms. We can formally define the nth Fibonacci number Fn as follows, starting with n = 0.

fibonacciRecursion

Since the Fn depends on the two previous terms, it’s necessary to have two base cases. The Fibonacci sequence is a special kind of Lucas sequence. Other Lucas sequences specify different values for the two base cases and sometimes coefficients to multiply the previous terms by.

19.2.3. Iteration vs. recursion

These mathematical definitions are interesting, but what’s their relationship to Java code? So far, we’ve considered algorithms that are iterative in nature: processing is performed as a sequence of operations on elements of a sequential data structure. We sum the elements of an array by iterating through them from first to last. We multiply two matrices by using nested for loops to sequence through the matrix contents in the proper order. Similarly, one method might make a sequence of one or more calls to other methods. We’re confident that such computations terminate because we start at the beginning and work to the end of a finite structure. But what if the structure is not a simple linear or multidimensional array? The path we’re trying to find through the maze is of unknown length and complexity.

A method might call other methods to complete its operation. For example, a method that sorts a list of String values calls another method to do pairwise comparison of the values in the list. A method that calls itself, either directly or indirectly, is called a recursive method.

A recursive method might seem like a circular argument that never ends. In fact, a recursive method only calls itself under certain circumstances. In other circumstances, it does not. A recursive method has the same two parts that a mathematical recursive definition has.

  • Base case
    The operation being computed is done without any recursive calls.

  • Recursive case
    The operation is broken down into smaller pieces, one or more of which results in a recursive call to the method itself.

Each time a method calls itself recursively it does so on a smaller problem. Eventually, it reaches a base case, and the recursion terminates.

A recursive method is useful when a problem can be broken down into smaller subproblems where each subproblem has the same structure as the original, complete problem. These subproblems can be solved by recursive calls and the results of those calls assembled to create a larger solution.

Recursive methods are often surprisingly small given their complexity. Each recursive call only makes a single step forward in the process of solving the problem. In fact, it can appear that the problem is never solved. The code has something like a “leap of faith” inside of it. Assuming you can solve a smaller subproblem, how do you put the solutions together to solve the full problem? This assumption is the leap of faith, but it works out as long as the subproblems get broken down into smaller and smaller pieces that eventually reach a base case.

From a theoretical standpoint, any problem that can be solved iteratively can be solved recursively, and vice versa. Iteration and recursion are equivalent in computational power. Sometimes it’s more efficient or more elegant to use one approach or the other, and some languages are designed to work better with a particular approach.

19.2.4. Call stack

Many programmers who are new to recursion feel uncomfortable about the syntax. How can a method call itself? What does that even mean?

Recursion in Java is grounded in the idea of a call stack. We discuss the stack abstract data type in Chapter 18. A similar structure is used to control the flow of control of a program as it calls methods.

Recall that a stack is a first in, last out (FILO) data structure. Each time a method is called, its local variables are put on the call stack. As the method executes, a pointer to the current operation it’s executing is kept on the call stack as well. This collection of local variables and execution details for a method call is called the stack frame or activation record. When another method is called, it pushes its own stack frame onto the call stack as well, and its caller remembers what it was executing before the call. When a method returns, it pops its stack frame (the variables and state associated with its execution) off the call stack.

A recursive method is called in exactly the same way. It puts another copy of its stack frame on the call stack. Each call of the method has its own stack frame and operates independently. There’s no way to access the variables from one call to the next, other than by passing in parameters or returning values.

Figure 19.3 shows the stack frames being pushed onto the call stack as the main() method calls the factorial() method, starting with the argument 4. The factorial() method recursively calls itself with successively smaller values.

recursivecalls
Figure 19.3 Successive recursive method calls getting added to the call stack.

Figure 19.4 shows the stack frames popping off the call stack as each call to factorial() returns. As the answers are returned, they’re incorporated into the answer that’s generated and returned to the next caller in the sequence until the final answer 24 (4!) is returned to main().

recursivereturns
Figure 19.4 Recursive methods returning results to their callers.

19.3. Syntax: Recursive methods

Unlike many Syntax sections in other chapters, there’s no new Java syntax to introduce here. Any method that calls itself, directly or indirectly, is a recursive method. Recursive methods are simply methods like any others, called in the normal way.

The real difficulty in learning to program recursively lies in breaking out of the way you’re used to thinking about program control flow. All that you’ve learned about solving problems with iteration in previous chapters might make it harder for you to embrace recursion.

Iteration views the whole problem at once and tries to sequence all the pieces of the solution in some organized way. Recursion is only concerned with the current step in the solution. If the current step is one in which the answer is clear, you’re in a base case. Otherwise, the solution takes one step toward the answer and then makes the leap of faith, allowing the recursion to take care of the rest. Programmers who are new to recursion are often tempted to do too much in each recursive call. Don’t rush it!

The use of recursion in languages like Java owes much to the development of functional programming. In many functional languages (such as Scheme), there are no loops, and only recursion is allowed. In a number of these languages, there’s no assignment either. Each variable has one value for its entire lifetime, and that value comes as a parameter from whatever method called the current method.

It may seem odd to you, but this approach is a good one to follow when writing recursive methods. Try not to assign variables inside your methods. See if the work done in each method can be passed on as an argument to the next method rather than changing the state inside the current method. In that way, each recursive method is a frozen snapshot of some part of the process of solving the problem. Of course, this guideline is only a suggestion. Many practical recursive methods need to assign variables internally, but a surprisingly large number do not.

Because the data inside these methods is tied so closely to the input parameters and the return values given back to the caller, these methods are often made static. Ideally, recursive methods do not change the state of fields or class variables. Again, sometimes changing external state is necessary, but recursive methods are meant to take in only their input parameters and give back only return values. Recursive code that reads and writes variables inside of objects or classes can be difficult to understand and debug since it depends on outside data.

With this information as background, we focus on examples for the rest of this section. Because recursion is a new way of thinking, approach these examples with an open mind. Many students have the experience that recursion makes no sense until they see the right example. Then, the way it works suddenly “clicks.” Don’t be discouraged if recursion seems difficult at first.

In this section, we work through examples of factorial computation, Fibonacci numbers, the classic Tower of Hanoi problem, and exponentiation. These problems are mathematical in nature because mathematical recursion is easy to model in code. The next section applies recursion to processing data structures.

Example 19.4 Factorial implemented recursively

In our first example of a recursive implementation, we return to the factorial function. Recall the recursive definition that describes the function.

factorialRecursion

By translating this mathematical definition almost directly into Java, we can generate a method that computes the factorial function.

public static long factorial(int n) {
	if(n == 0)     // Base case
		return 1;
	else            // Recursive case
		return n * factorial(n-1);
}

Note the base case and recursive case are exactly the same as in the recursive definition. The return type of the method is long because factorial grows so quickly that only the first few values are small enough to fit inside of an int.

Example 19.5 Fibonacci implemented recursively

Let’s return to the recursive definition of Fibonacci.

fibonacciRecursion

Like factorial, this definition translates naturally into a recursive method in Java.

public static int fibonacci(int n) {
    if(n == 0 || n == 1)  // Base cases
        return 1;
    else                  // Recursive case
        return fibonacci(n-1) + fibonacci(n-2);
}

One significant problem with this implementation is performance. In this case, the double recursion performs a great deal of redundant computation.

One technique for eliminating redundant computation in recursion is called memoization. Whenever the value for a subproblem is computed, we note down the result (like a memo). When we go to compute a value, we first check to see if we have already found it.

To perform memoization for Fibonacci, we can pass an array of int values of length n + 1. The values in this array all begin with a value of 0. When computing the Fibonacci value for a particular n, we first check to see if its value is in the array. If not, we perform the recursion and store the result in the array.

public static int fibonacci(int n, int[] results) {
    if(results[n] == 0) {
        if(n == 0 || n == 1)
            results[n] = 1;
        else
            results[n] = fibonacci(n-1) + fibonacci(n-2);
    }
    return results[n];
}

This change makes the computation of the nth Fibonacci number much more efficient; however, even more efficient approaches are described in the exercises.

Example 19.6 Tower of Hanoi

The famous Tower of Hanoi puzzle is another example commonly used to illustrate recursion. In this puzzle, there are three poles containing a number of different sized disks. The puzzle begins with all disks arranged in a tower on one pole in decreasing size, with the smallest diameter disk on top and the largest on the bottom. Figure 19.5 shows an example of the puzzle. The goal is to move all the disks from the initial pole to the final pole, with two restrictions.

  1. Only one disk can be moved at a time.

  2. A larger disk can never be placed on top of a smaller disk.

hanoi
Figure 19.5 Tower of Hanoi puzzle with 4 disks on the initial pole.

The extra pole is used as a holder for intermediate moves. The idea behind the recursive solution follows.

  • Base Case
    Moving one disk is easy. Just move it from the pole it’s on to the destination pole.

  • Recursive Case
    In order to move n > 1 disks from one pole to another, we can move n - 1 disks to an intermediate pole, move the nth disk to the destination pole, and then move the n - 1 disks from the intermediate pole to the destination pole.

The Tower of Hanoi solution in Java translates this outline into code.

Program 19.1 Recursive solution to the Tower of Hanoi with four disks and poles named 'A', 'B', and 'C'.
public class TowerOfHanoi {
    public static void main(String[] args) {
        move(4, 'A', 'C', 'B');
    }
    
    public static void move(int n, char fromPole, char toPole, char viaPole) {
        if(n == 1)
            System.out.format("Move disk from pole %c to pole %c.\n",
                fromPole, toPole);
        else {
            move(n - 1, fromPole, viaPole, toPole);
            move(1, fromPole, toPole, viaPole);
            move(n - 1, viaPole, toPole, fromPole);
        }
    }
}

A legend tells of monks that are solving the Tower of Hanoi puzzle with 64 disks. The legend predicts that the world will end when they finish. Run the implementation above with different numbers of disks to see how long the sequence of moves is. Try small numbers of disks, since large numbers of disks take a very long time.

Example 19.7 Exponentiation

Both Fibonacci and the Tower of Hanoi have natural recursive structures. In the case of Fibonacci, one way to implement its natural recursive definition results in very wasteful computation. In the case of the Tower of Hanoi, the only way to solve the problem takes an excruciatingly long amount of time.

However, we can apply recursion to many practical problems and get efficient solutions. Consider the problem of exponentiation, which looks trivial: Given a rational number a and a positive integer n, find the value of an.

It’s tempting to call Math.pow(a, n) or to use a short for loop to compute this value, but what if neither tool existed in Java? A simple recursive formulation can describe exponentiation.

exponentRecursion1

As with factorial and Fibonacci, directly converting the recursive definition into Java syntax yields a method that computes the correct value.

public static double power(double a, int n) {
    if(n == 1)    // Base case
        return a;
    else          // Recursive case
        return a*power(a, n - 1);
}

Admittedly, this method only works for positive integer values of n. Ignoring that limitation, what can we say about its efficiency? For any legal value of n, the method is called n times. If n has a small value, like 2 or 3, the process is quick. But if n is 1,000,000 or so, the method might take a while to finish. Another problem is that stack size is limited. On most systems, the JVM crashes with a StackOverflowError if a method tries to call itself recursively 1,000,000 times.

If we limit n to a power of 2, we can do something clever that makes the method much more efficient with many fewer recursive calls. Consider this alternative recursive definition of exponentiation.

exponentRecursion2

Recalling basic rules of exponents, it’s true that an = (an/2)2, but what does that buy us? If we structure our method correctly, we cut the size of n in half at each recursive step instead of only reducing n by 1.

public static double power(double a, int n) {
    if(n == 1)    // Base case
        return a;
    else {          // Recursive case
        double temp = power(a, n/2);
        return temp*temp;
    }
}

Note that we only make the recursive call once and save its result in temp. If we made two recursive calls, we would no longer be more efficient than the previous method. That version took n recursive calls. How efficient is this version? The answer is the number of times you have to cut n in half before you get 1. Let’s call that value x. Recall that n is a power of 2, meaning that n = 2k for some integer k ≥ 0.

logarithm

In other words, the number of times you have to divide n in half to get 1 is the logarithm base 2 of n, written log2 n. The logarithm function is the inverse of exponentiation. It cuts any number down to size very quickly (just as exponentiation blows up the value of a number very quickly). For example, 220 = 1,048,576. Thus, log2 1,048,576 = 20. The original version of power() would have to make 1,048,576 calls to raise a number to that power. This second version would only have to make 20 calls.

It’s critical that n is a power of 2 (1, 2, 4, 8, …​); otherwise, the process of repeatedly cutting n in half loses some data due to integer division. The problem is that, at some point in the recursion, the value of n will become odd unless you start with a power of 2. There’s a way to extend this clever approach to all values of n, even and odd, but we leave it as an exercise.

Recursion offers elegant ways to compute mathematical functions like those we’ve explored in this section. Recursion also offers powerful ways to manipulate data structures. As we show in the next section, recursive methods are especially well suited to use with recursive data structures.

19.4. Syntax: Recursive data structures

Because recursion can be used to do anything that iteration can do, it’s clear that data structures can be processed recursively. For example, the following recursive method reverses the contents of an array. It keeps track of the position it’s swapping in the array with the position parameter. This method is initially called with a value of 0 passed as an argument for position.

public static void reverse(int[] array, int position) {
	if(position < array.length/2) {
		int temp = array[position];
		array[position] = array[array.length - position - 1];
		array[array.length - position - 1] = temp;
		reverse(array, position + 1);
	}
}

Note that nothing is done in the base case for this recursive method. The recursion swaps the first element of the array (at index 0) with the last (at index array.length - 1). Recursion continues until position has reached half the length of array. If execution continued past the halfway point, it would begin to re-swap elements that had already been swapped.

Although it’s possible to reverse an array recursively, there’s usually no advantage in doing so. We introduced bubble sort and selection sort in previous chapters, but neither of these algorithms is very fast. Many of the best sorting algorithms are recursive, as in the following example of merge sort.

Example 19.8 Merge sort

Merge sort is an efficient sorting algorithm that’s often implemented recursively. The idea of the sort is to break a list of items in half and recursively merge sort each half. Then, these two sorted halves are merged back together into the final sorted list. The base case of the recursion is when there’s only a single item in the list, since a list with only one thing in it is, by definition, sorted.

Here’s a method that recursively sorts an int array using the merge sort algorithm.

public static void mergeSort(int[] array) {
	if(array.length > 1) {
		int[] a = new int[array.length/2];
		int[] b = new int[array.length - a.length];
		for(int i = 0; i < a.length; ++i)
			a[i] = array[i];
		for(int i = 0; i < b.length; ++i)
			b[i] = array[a.length + i];
		mergeSort(a);
		mergeSort(b);
		merge(array, a, b);
	}
}

The mergeSort() method is quite short and appears to do very little. It starts by creating arrays a and b and copying roughly half of the elements in array into each. We make a half the size of array, but we can’t do the same thing for b because an odd length for array would leave us without enough space in a and b to hold everything from array. Instead, we let b hold however much is leftover after the elements for a have been accounted for.

Then, arrays a and b are recursively sorted. Finally, these two sorted arrays are merged back into array in sorted order using a helper method called merge(). This method is non-recursive and does much of the real work in the algorithm.

public static void merge(int[] array, int[] a, int[] b) {
    int aIndex = 0;
    int bIndex = 0;
    for(int i = 0; i < array.length; ++i) {
        if(bIndex >= b.length)
            array[i] = a[aIndex++];
        else if(aIndex >= a.length)
            array[i] = b[bIndex++];
        else if(a[aIndex] <= b[bIndex])
            array[i] = a[aIndex++];
        else
            array[i] = b[bIndex++];
    }
}

The merge() method loops through all the elements in array, filling them in. We keep two indexes, aIndex and bIndex, that keep track of our current positions in the a and b arrays, respectively. This method assumes that a and b are sorted and that the sum of their lengths is the length of array. We want to compare each element in a and b, always taking the smaller and putting it into the next available location in array. Since the next smallest item could be in either a or b, we never know when we’ll run out of elements in either array. That’s why the first two if statements in the merge() method check to see if the bIndex or the aIndex is already past the last element in its respective array. If so, the next element from the other array is automatically used. By the time the third if statement is reached, we’re certain that both indexes are valid and can compare the elements at those locations to see which is smaller.

Sorting lists using the merge sort algorithm seems more complicated than using bubble sort or selection sort, but this additional complication pays dividends. For large lists, merge sort performs much faster than either of those sorts. In fact, its speed is comparable to the best general sorting algorithms that are possible.

Although recursive sorting algorithms are useful for arrays, recursion really shines when manipulating recursive data structures. A recursive data structure is one that’s defined in terms of itself. For example, class X is recursive if there’s a field inside X with type X.

public class X {
    private int a, b;
    private X x;
}

The linked list examples from Chapter 18 are recursive data structures, since a linked list node is defined in terms of itself. You might not have thought of the linked list Node class as being recursive since it simply has a reference to another Node inside it. However, this self-reference is the essence of a recursive data structure.

Data structures are often defined recursively. We typically need to represent an unbounded collection of data, but we always write bounded programs to describe the data. A recursive data structure allows us to bridge the gap between a compile-time, fixed-length definition and a run-time, unbounded collection of objects.

Recursive data structures have a base case to end the recursion. Typically, the end of the recursion is indicated by a link with a null value. For example, in the last node of a linked list, the next field is null. Unsurprisingly, recursive methods are frequently used to manipulate recursive data structures.

Example 19.9 Recursive linked list size

How would you get the size of a linked list? The implementation in Program 18.5 keeps track of its size as it grows, but what if it didn’t? A standard way to count the elements in the list would be to start with a reference to the head of the list and a counter with value zero. As long as the reference is not null, add one to the counter and set the reference to the next element on the list. Program 19.2 counts the elements in this way.

Program 19.2 Linked list implementation whose size() method counts its elements iteratively.
public class IterativeListSize {
    private static class Node {
        public String value;
        public Node next;
    }
 
    private Node head = null;   
    
    public void add(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;        
    }
    
    public int size() {
        Node current = head;
        int counter = 0;
        while(current != null) {
            current = current.next;
            counter++;
        }           
        return counter;
    }
}

An alternative way to count the number of elements in a linked list is to use the natural recursion of the linked list itself. We can say that the length of a linked list is 0 if the list is empty (the current link is null); otherwise, it’s one more than the size of the rest of the list.

Program 19.3 counts the elements in a linked list using this recursive procedure. Note that there’s a non-recursive size() method that calls the recursive size() method. This non-recursive method is called a proxy method. The recursive method requires access to the internals of the data structure. The proxy method calls the recursive method with the appropriate starting point (head), while providing a public way to get the list’s size without exposing its internals.

Program 19.3 Linked list implementation with a recursive size() method for counting its elements.
public class RecursiveListSize {
    private static class Node {
        public String value;
        public Node next;
    }
 
    private Node head = null;   
    
    public void add(String value) {
        Node temp = new Node();
        temp.value = value;
        temp.next = head;
        head = temp;        
    }
    
    // Proxy method
    public int size() {
        return size(head);
    }
    
    private static int size(Node list) {
        if(list == null) 				// Base case
            return 0;   
        else
			return 1 + size(list.next);	// Recursive case
    }    
}

19.4.1. Trees

A linked list models a linear, one-to-one relationship between its elements since each item in the list is linked to a maximum of one following item. Another useful relationship to model is a hierarchical, one-to-many relationship: parent to children, boss to employees, directory to files, and so on. These relationships can be modeled using a tree structure, which begins with a single root, and proceeds through branches to the leaves. Typically, the elements of a tree are also called nodes, with the three following special cases.

  • Root node
    The root of the tree has no parents.

  • Leaf node
    A leaf is at the edge of a tree and has no children.

  • Interior node
    An interior node has a parent and at least one child. It’s neither the root nor a leaf.

Figure 19.6 shows a visualization of a tree. In nature, a tree has its root at the bottom and branches upward. Since the root is the starting point for a tree data structure, it’s almost always drawn at the top.

treevisualization
Figure 19.6 Visualization of a tree. The root is shown in light red. The leaves are shown in blue. The interior nodes are show in light purple.

Abstractly, a tree is either empty (the base case) or contains references to 0 or more other trees (the recursive case). Trees are useful for storing and retrieving sorted data efficiently. Some applications include dictionaries, catalogs, ordered lists, and any other sorted set of objects. For these purposes, we can define an abstract data type that includes operations such as add() and find().

A special case of a tree that’s used frequently is a binary tree, in which each node references at most two other trees.

Example 19.10 Binary search tree

A binary search tree is a further special case of a binary tree with the following three properties.

  1. The value in the left child of the root is smaller than the value in the root.

  2. The value in the right child of the root is larger than the value in the root.

  3. Both the left and the right subtrees are binary search trees.

This recursive definition describes a tree that makes items with a natural ordering easy to find. If you’re looking for an item, you first look at the root of the tree. If the item you want is in the root, you’ve found it! If the item you want is smaller than the root, go left. If the item you want is larger than the root, go right. If you ever run out of tree nodes by hitting a null, the item isn’t in the tree.

This example is a simple binary tree that can store a list of String values and print them out in alphabetical order. Program 19.4 shows the Tree class that defines the fields and two public methods, add() and print(), that operate on the tree. Each is a proxy method that calls its private recursive version, which takes a reference to a Node object. The Node static nested class contains three fields.

  • value: the String value stored at the node

  • left: a link to the left subtree

  • right: a link to the right subtree

Program 19.4 Class that implements a simple binary search tree ADT for creating a sorted list of Strings values.
public class Tree {
    private static class Node {
        public String value;
        public Node left = null;
        public Node right = null;
    }
    
    private Node root = null;
        
    // Proxy add
    public void add(String value) {
        root = add(value, root);
    }
    
    private static Node add(String value, Node tree) {
        if(tree == null) { // Base case (1)
            tree = new Node();
            tree.value = value;
        }
        // Left recursive case    (2)
        else if(value.compareTo(tree.value) < 0)
            tree.left = add(value, tree.left);
        // Right recursive case   (3)
        else if(value.compareTo(tree.value) > 0)
            tree.right = add(value, tree.right);
        return tree; 			  (4)
    }
    
    // Proxy print
    public void print() {
        print(root);
    }
        
    private static void print(Node tree) {
        if(tree != null) {
            print(tree.left); 				(5)
            System.out.println(tree.value);	(6)
            print(tree.right);				(7)
        }
    }
}
1 The recursive add() method first checks to see if the current subtree is empty (null). If so, it creates a new Node and puts value inside it.
2 If the current subtree is not null, it checks to see if value is smaller or larger than the value at the root of the subtree. If it’s smaller, it recurses down the left subtree.
3 If it’s larger, it recurses down the right subtree. If value is already in the root node, it does nothing.
4 Remember that all parameters are pass by value in Java. Thus, assigning a new Node to tree doesn’t by itself change anything at higher levels of the tree. What does change the links in the parent of the current subtree is returning the tree pointer. If the recursive call to add() was made with a left or a right subtree, the left or right link, respectively, of the parent Node is assigned the return value. If the call was made with root, the parent of the entire tree, the non-recursive add() method sets its value when the recursive add() returns.
5 The recursive print() method starts by walking down the left subtree. Those values are all alphabetically less than the value of the current node.
6 When it finishes, it prints the current node value.
7 Finally, it walks the right subtree to print the values that alphabetically follow the value in the current node. This path through the nodes of the tree is called an inorder traversal.

Figure 19.7 shows a visualization of the contents of this implementation of a binary search tree. As with a linked list, an “X” is used in place of arrows that point to null.

treeclasses
Figure 19.7 Visualization of a tree implementation with classes.

With the power of a binary search tree, it takes virtually no code at all to store a list of String values and then print them out in sorted order. Program 19.5 gives an example of this process using a Tree object for storage.

Program 19.5 Reads String values, stores them in a binary search tree, and prints the results in sorted order.
import java.util.Scanner;

public class ReadAndSortStrings {
	public static void main(String[] args) {
		Tree tree = new Tree();
		Scanner in = new Scanner(System.in);
		
		while(in.hasNextLine())           
			tree.add(in.nextLine());        
		tree.print();
	}
}

Binary search trees (and other trees, including heaps, tries, B-trees, and more) are fundamental data structures that have been studied heavily. Designing them to have efficient implementations that balance the size of their left and right subtrees is an important topic that’s beyond the scope of this book.

19.4.2. Generic dynamic data structures and recursion

Combining dynamic data structures and generics from the previous chapter and recursion from this chapter gives us the full power of generic dynamic data structures and recursive methods to process them.

Example 19.11 Binary search tree to hold integers

Consider Program 19.6, which implements a tree that stores values of type Integer. Although it would be more efficient to store int values, we use the Integer wrapper class to ease our eventual transition into a parameterized generic type.

Program 19.6 Variant of Program 19.4 that stores Integer values instead of String values.
public class IntegerTree {
    private static class Node {
        Integer value;
        Node left = null;
        Node right = null;
    }
    
    private Node root = null;
        
    // Proxy add
    public void add(Integer value) {
        root = add(value, root);
    }
    
    private static Node add(Integer value, Node tree) {
        if(tree == null) { // Base case
            tree = new Node();
            tree.value = value;
        }
        // Left recursive case
        else if(value.compareTo(tree.value) < 0)
            tree.left = add(value, tree.left);
        // Right recursive case
        else if(value.compareTo(tree.value) > 0)
            tree.right = add(value, tree.right);
        return tree;        
    }
    
    // Proxy print
    public void print() {
        print(root);
    }
        
    private static void print(Node tree) {
        if(tree != null) {
            print(tree.left);
            System.out.println(tree.value);
            print(tree.right);
        }
    }
}

It’s a waste to create class IntegerTree, which is identical to Tree except that the type String has been replaced by Integer. As in Section 18.5, we want our data structures, recursive or otherwise, to hold any type. In this way, we can reuse code across a wide range of applications.

Example 19.12 Defining a generic binary search tree

Program 19.7 defines a generic version of the Tree class. This example is complicated by the fact that we need to be able to compare the value we want to store with the value in each Node object. We can’t make a tree with any arbitrary type. Objects of the type must have the ability to be compared to each other and ordered. Thus, we use a bounded type parameter specifying that the type T stored in each Tree must implement the Comparable interface. This requirement complicates the generic syntax significantly but guarantees that any type that cannot be compared with itself is rejected at compile-time.

Program 19.7 Class that implements a generic tree.
public class GenericTree<T extends Comparable<T>> {
    private class Node {
        T value;
        Node left = null;
        Node right = null;
    }
    
    private Node root = null;
        
    // Proxy add
    public void add(T value) {
        root = add(value, root);
    }
    
    private Node add(T value, Node tree) {
        if(tree == null) { // Base case
            tree = new Node();
            tree.value = value;
        }
        // Left recursive case
        else if(value.compareTo(tree.value) < 0)
            tree.left = add(value, tree.left);
        // Right recursive case
        else if(value.compareTo(tree.value) > 0)
            tree.right = add(value, tree.right);
        return tree;        
    }
    
    // Proxy print
    public void print() {
        print(root);
    }
        
    private void print(Node tree) {
        if(tree != null) {
            print(tree.left);
            System.out.println(tree.value);
            print(tree.right);
        }
    }
}

First, note that the Node class and the recursive methods are no longer static. The generic syntax for keeping them static without producing compiler warnings is unnecessarily complex. The type specifier T extends Comparable<T> guarantees that type T implements the interface Comparable<T>. The generic Comparable interface defined in the Java API is as follows.

public interface Comparable<T> {
	int compareTo(T object);
}

The syntax for generics in Java with type bounds is complicated, and we only scratch the surface here. The good news is that these subtleties are more important for people designing data structures and libraries and come up infrequently for programmers who are only using the libraries.

Example 19.13 Using a generic class

Program 19.8 uses the generic tree class to create two kinds of trees, a tree of String objects and a tree of Integer objects. Java library implementations of binary search trees are available as the TreeSet and TreeMap classes.

Program 19.8 Creates two trees with different underlying types.
import java.util.Random;
import java.util.Scanner;

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

        GenericTree<String> stringTree = new GenericTree<>();
        GenericTree<Integer> integerTree = new GenericTree<>();
        
        while(in.hasNextLine())
            stringTree.add(in.nextLine());      
        stringTree.print();
        
        Random random = new Random();
        for (int i = 0; i < 10; i++)
            integerTree.add(random.nextInt());      
        integerTree.print();
    }
}

19.5. Solution: Maze of doom

Our algorithm for solving the maze follows the conventional pencil-and-paper method: trial and error! We mark locations in the maze with '*' as we explore them. If we come to a dead end, we unmark the location (by replacing '*' with ' ') and return to our previous location to try a different direction.

We start at the beginning square of the maze, which must be a passageway. We mark that location as part of the path by putting '*' in the cell. Now, what can we do? There are, in general, four possible directions to head: up, down, left, or right. If that direction doesn’t take us outside the bounds of the array, then we find either a wall or a passageway. If we’ve been walking through the maze, we might also find a part of the current path (often the square we were on before the current one).

Suppose from our current point in the maze we could send a scout ahead in each of the four directions. If the direction didn’t take the scout out of bounds, he would find either a wall, a part of the current path (the path that led into that space), or an open passageway. If the scout doesn’t find an open passageway, he reports back that that direction doesn’t work.

On the other hand, if the scout finds an open passageway, what does he do? Brace yourself! He does the exact same thing we just did: send out scouts of his own in each of the four possible directions.

With careful, consistent coding, the scout follows the exact same process we did. And the scout’s scouts. And so on. There is, in fact, only one method and instead of calling a scout method to investigate each of the squares in the four directions, we call our own method recursively.

import java.util.Scanner;

public class MazeSolver {
    private char[][] maze; 			 			(1)
    private final int ROWS, COLUMNS; 			(2)

    public static void main(String[] args) {
        MazeSolver solver = new MazeSolver();	(3)
        if(solver.solve(0, 0))
            System.out.println("\nSolved!");
        else
            System.out.println("\nNot solvable!");
        solver.print();							(4)
    }   
1 The MazeSolver class needs a two-dimensional array of char values to store a representation of the maze.
2 It’s also convenient to store the number of rows and columns as constant fields.
3 The main() method creates a new MazeSolver object and then calls its solve() method with a starting location of (0, 0). It prints an appropriate message depending on whether or not the maze was solved.
4 Finally, it prints out the maze, which includes a path marked with '*' symbols if the maze is solvable.
    public MazeSolver() {
        Scanner in = new Scanner(System.in);	(1)
        ROWS = in.nextInt();					(2)
        COLUMNS = in.nextInt();
        in.nextLine();
        maze = new char[ROWS][COLUMNS];			(3)
        for(int row = 0; row < ROWS; row++) {	(4)
            String line = in.nextLine();
            System.out.println(line);
            for (int column = 0; column < COLUMNS; column++)
                maze[row][column] = line.charAt(column);
        }
    }
1 The constructor for MazeSolver creates a Scanner. It assumes that the file describing the maze is redirected from standard input, although it would be easy to modify the constructor to take a file name and read from there instead.
2 Next, it reads two integers and sets the ROWS and COLUMNS to those values, which will be constants moving forward.
3 It allocates a two-dimensional array of char values with ROWS rows and COLUMNS columns.
4 Finally, it reads through the file, storing each line of char values into this array. As it reads, it prints out each line to the screen, showing the initial (unsolved) maze.
    public void print() {
        for(int row = 0; row < ROWS; row++) {
            for (int column = 0; column < COLUMNS; column++)
                System.out.print(maze[row][column]);
            System.out.println();
        }
    }

The print() method is a utility method that prints out the maze. It iterates through each row, printing out the values for the columns in that row.

    public boolean solve(int row, int column) {
		if(row < 0 || column < 0 || row >= ROWS || column >= COLUMNS)
			return false;
		else if(maze[row][column] == 'E')
			return true;
		else if(maze[row][column] != ' ')
			return false;
		else {
			maze[row][column] = '*';
			if(solve(row - 1, column) || solve(row + 1, column) ||
				solve(row, column - 1) || solve(row, column + 1))
				return true;
			else {
				maze[row][column] = ' ';
				return false;
			}
		}
    }
}

The heart of the solution is the recursive method solve(). The solve() method takes two parameters, row and column, and tries to find a solution to the maze starting at location maze[row][column]. It assumes that the maze is filled with '+' for walls, ' ' for passageways, and may include '*' characters at locations that are part of the partially completed solution.

If solve() is able to find a solution from the current location, it returns true, otherwise it returns false. There are three base cases for the current location in the maze.

  1. The current location is outside the maze. Return false.

  2. The current location is the ending location (marked with 'E'). We have a winner, return true!

  3. The current location is not a passage (either a wall or a location in the current path that’s already been marked). This call to solve is not making progress toward the finish. Return false.

If none of the base cases applies, then the current location, which must contain a ' ' character, might be on a successful path, so solve() gives it a try. The method tentatively marks the current position with '*'. Then, it tries to find a path from the current location to each of the four neighboring cells by recursively calling solve(). If any of those four neighbors returns true, then solve() knows it’s found a completed path and returns true to its caller.

If none of the four neighbors was on a path to the destination, then the current location is not on a path. The method unmarks the current location (by storing a ' ') and returns false. Presumably, its caller figures out what to do next, perhaps calling a different one of its neighbors or giving up and returning false to its caller.

The very first call to solve() from the main() method either returns true if a complete path through the maze is found or false if no path exists. Note that this solver has no guarantee of finding the shortest path through the maze, but if there’s at least one path to the goal, it’ll find one.

19.6. Concurrency: Futures

This section doesn’t deal explicitly with recursion, but it does deal with concurrency and methods in an interesting way. When we call a method in Java, a stack frame for the method is put on the stack, and the thread of execution begins executing code inside the method. When it’s done, it returns a value (or not), and execution resumes in the caller. But what if calling the method began executing an independent thread, and the caller continued on without waiting for the method to return?

This second situation should seem familiar, since it’s exactly what happens when the start() method is called on a Thread object: the start() method returns immediately to its caller, but an independent thread of execution has begun executing the code in the run() method of the Thread.

What if we only care about the value that’s computed by the new thread of execution? We can think of spawning the thread as an asynchronous method call, a value that’s computed at some point rather than one we have to wait for. The name for such an asynchronous method call is a future. In some languages, particularly functional languages, all concurrency is expressed as a future. In Java, only a little bit of code is needed to create threads that can behave like futures. However, the idea of futures is pervasive enough that Java API tools were created to make the process of creating them simple.

We introduce three interfaces and a factory method call that can allow you to use futures in Java. This section is not a complete introduction to futures, but the tools presented are enough to get you started.

The first interface is the Future interface, which allows you to store a reference to the asynchronous computation while it’s computing, before you ask for its value. The second interface is the Callable interface, which is similar to the Runnable interface in that it allows you to specify a class whose objects can be started as independent threads. Both the Future and Callable interfaces are generic interfaces that require you to specify a type. Remember that futures are supposed to give back an answer, and that’s the type you supply as a parameter. For example, when creating a future that returns an int value, you would create a class that implements the Callable<Integer> interface, requiring it to contain a method with the signature Integer call(). Likewise, you would store a reference to the future you create in a Future<Integer> reference.

And how do you create such a future? Usually, many futures are running at once to leverage the power of multiple cores. What if you want to create 100 futures but only have 8 cores? The process of creating threads is expensive, and it might not be worthwhile to create 100 threads when only 8 are able to run concurrently. To deal with this problem, the Java API provides classes that implement the ExecutorService interface, which can maintain a fixed-size pool of threads. When a thread finishes computing one future, it’s automatically assigned another. To create an object that can manage threads this way, call the static factory method newFixedThreadPool() on the Executors class with the size of the thread pool you want create. For example, we can create an ExecutorService with a pool of 8 threads as follows.

ExecutorService executor = Executors.newFixedThreadPool(8);

Once you have an ExecutorService, you can give it a Callable object of a particular type (such as Callable<Integer>) as a parameter to its submit() method, and it will return a Future object of a matching type (such as Future<Integer>). Then, the future is running (or at least scheduled to run). At any later point you can call the get() method on the Future object, which returns the value of its computation. Like calling join(), calling get() is a blocking call that might have to wait for the future to finish executing.

All this messy syntax should become clearer in the following example which uses futures to compute the sum of the square roots of the first 100,000,000 integers concurrently.

Example 19.14 Using futures to sum square roots

To use futures to sum the square roots of integers, we first need a worker class that implements Callable. Since the result of the sum of square roots is a double, it must implement Callable<Double>. Recall that primitive types such as double can’t be used as generic type parameters, requiring us to use wrapper classes in those cases.

import java.util.concurrent.*;				(1)

public class RootSummer implements Callable<Double> {
    private int min;
    private int max;
    
    public RootSummer(int min, int max) { 	(2)
        this.min = min;
        this.max = max;
    }   
    
    public Double call() { 					(3)
        double sum = 0.0;
        for(int i = min; i < max; ++i)
            sum += Math.sqrt(i);
        return sum;
    }
}
1 First, we import java.util.concurrent.* to have access to the Callable interface.
2 RootSummer is a simple worker class that takes a min and a max value in its constructor.
3 Its call() method returns the sum of the square roots of all the int values greater than or equal to min and less than max.

Of course, we need another class to create the ExecutorService, start the futures running, and collect the results.

import java.util.concurrent.*; (1)
import java.util.ArrayList;

public class RootFutures {
    private static final int THREADS = 10; (2)
    private static final int N = 100000000;
    private static final int FUTURES = 1000;
    
    public static void main(String[] args) {
        ArrayList<Future<Double>> futures = new ArrayList<>(FUTURES);     (3)
        ExecutorService executor = Executors.newFixedThreadPool(THREADS); (4)
        int work = N/FUTURES; (5)
1 The first part of RootFutures is setup. The imports give us the concurrency tools we need and a list to store our futures in.
2 We have three constants. THREADS specifies the number of threads to create. N gives the number we’re going up to. FUTURES is the total number of futures we create, considerably larger than the number of threads they share.
3 Inside the main() method, we create an ArrayList to hold the futures. Since we know the number of futures ahead of time, an array would be ideal. Unfortunately, quirks in the way Java handles generics make it illegal to create an array with a generic type. Instead, we create an ArrayList with the size we’ll need pre-allocated.
4 Next, we create an ExecutorService with a thread pool of size THREADS.
5 Finally, we find the amount of work done by each future by dividing N by FUTURES. We can use simple division in this case instead of a more complicated load-balancing approach because 100,000,000 is divisible by 10.
        System.out.println("Creating futures...");
        for(int i = 0; i < FUTURES; i++) {
            Callable<Double> summer = new RootSummer(1 + i*work, 1 + (i + 1)*work); (1)
            Future<Double> future = executor.submit(summer); (2)
            futures.add(future);
        }
1 To create the futures, we first instantiate a RootSummer object with the appropriate bounds for the work it’s going to compute.
2 Then, we supply that object to the submit() method on the ExecutorService, which returns a Future object. We could have saved a line of code by storing this return value directly into the list futures.
        System.out.println("Getting results from futures...");
        double sum = 0.0;
        for(Future<Double> future: futures) { (1)
            try {
                sum += future.get(); (2)
            }
            catch(InterruptedException | ExecutionException e) { (3)
                e.printStackTrace();
            }
        }
        executor.shutdown(); (4)
        System.out.println("The sum of square roots is: " + sum); (5)
    }
}
1 To collect the values from each future, we iterate through the list of futures with an enhanced for loop.
2 We add the return value of each future’s get() method to our running total sum.
3 Because get() is a blocking call, we have to catch an InterruptedException in case we’re interrupted while waiting for the future to respond. However, we also have to catch an ExecutionException in case an exception occurs during the execution of the future. This exception handling mechanism is one of the big advantages of using futures: Exceptions thrown by the future are propagated back to the thread that gets the answer from the future. Normal threads simply die if they have unhandled exceptions. Note that we use the modern exception-catching syntax to handle two unrelated exceptions with the same catch block.
4 After all the values have been read and summed, we shut the ExecutorService down. If we’d wanted, we could have submitted additional Callable objects to it to run more futures.
5 Finally, we print out the result.

19.7. Exercises

Conceptual Problems

  1. Example 19.1 gave a mathematical recursive definition for x â‹… y. Give a similar recursive definition for x + y, assuming that y is a positive integer. The structure is similar to the recursion to determine your current age given in Section 19.2.1.

  2. In principle, every problem that can be solved with an iterative solution can be solved with a recursive one (and vice versa). However, the limited size of the call stack can present problems for recursive solutions with very deep recursion. Why? Conversely, are there any recursive solutions that are impossible to turn into iterative ones?

  3. Consider the first (non-memoized) recursive version of the Fibonacci method given in Example 19.5. How many times is fibonacci() called with argument 1 to compute fibonacci(n)? Instrument your program and count the number of calls for n = 2, 3, 4, …​, 20.

  4. In the recursive solve() method in the MazeSolver program given in Section 19.5, the current location in the maze array is set to a space character (' ') if no solution is been found. What value was in that location? How would the program behave if the value wasn’t changed back?

Programming Practice

  1. Exercise 8.11 from Chapter 8 challenges you to write a method to determine whether a String is a palindrome. Recall that a palindrome (if punctuation and spaces are ignored) can be described as an empty String or a String in which the first and last characters are equal, with all the characters in between forming a palindrome. Write a recursive method with the following signature to test if a String is a palindrome.

    public static boolean isPalindrome(String text, int start, int end)

    In this method, the start parameter is the index of the first character you’re examining, and the end parameter is the index immediately after the last character you’re examining. Thus, it would initially be called with a String, 0, and the length of the String, as follows.

    boolean result = isPalindrome("A man, a plan, a canal: Panama", 0, 30);
  2. The efficient implementation of Fibonacci from Example 19.5 eliminates redundant computation through memoization, storing values in an array as they’re found. However, it’s possible to carry along the computations of the previous two Fibonacci numbers without the overhead of storing an array. Consider the following method signature.

    public static int fibonacci(int previous, int current, int n)

    The next recursive call to the fibonacci() method passes in n - 1 and suitably altered versions of previous and current. When n reaches 0, the current parameter holds the value of the Fibonacci number you were originally looking for.

    The method would be called as follows for any value of n.

    int result = fibonacci(0, 1, n);

    Complete the implementation of this recursive method.

  3. Write an implementation of fast exponentiation that works for even and odd n. This implementation is exactly the same as the one given at the end of Example 19.7 except when n is odd. Use the following recursive definition of exponentiation to guide your implementation.

    exponentRecursion3
  4. Example 19.5 shows two implementations that can be used to find the nth Fibonacci number. With some knowledge of recurrence relations, it’s possible to show that there’s a closed-form equation that gives the nth Fibonacci number Fn where F0 = F1 = 1.

    fibonacciClosed

    Although this equation is a bit ugly, you can plug numbers into it to discover the value of Fn quickly, provided that you have an efficient way to raise values to the nth power. Use the recursive algorithm for fast exponentiation from Exercise 19.7 to make an implementation that finds the nth Fibonacci number very quickly.

    Note that this approach uses real numbers (including the square root of 5) that need to be represented as double values. There are exact methods that use fast exponentiation of integer matrices to do this computation without doing any floating-point arithmetic, but we won’t go into those details here.

  5. Example 19.9 shows a way to calculate the size of a linked list recursively. Add a recursive method called print() to the RecursiveListSize class that prints out the values in the linked list recursively, one value per line.

  6. Expand the previous exercise to add another method called reversePrint() that prints out the values in the linked list in the opposite order that they appear. It should take only a slight modification of the print() method you’ve already written.

  7. Create a recursive find() method (and a non-recursive proxy method to call it) for the Tree class given in Program 19.4. Its operation is similar to the add() method. If the subtree it’s examining is empty (null), it should return false. If the value it’s looking for is at the root of the current subtree, it should return true. These are the two base cases. If the value it’s looking for comes earlier in the alphabet than the value at the root of the current subtree, it should look in the left subtree. If the value it’s looking for comes later in the alphabet than the value at the root of the current subtree, it should look in the right subtree. Those are the two recursive cases.

  8. The height of a binary tree is defined as the longest path from the root to any leaf node. Thus, the height of a tree with only a root node in it is 0. By convention, the height of an empty tree is -1.

    Create a recursive getHeight() method (and a non-recursive proxy method to call it) for the Tree class given in Program 19.4. The base case is an empty tree (a null pointer), which has a height of -1. For the recursive case of a non-empty tree, its height is one more than the height of the larger of its two subtrees.

  9. Create a Java interface that describes a tree ADT. Modify the programs in Example 19.10 to implement this interface.

Experiments

  1. Write an iterative version of the factorial function and compare its speed to the recursive version given in the text. Use the System.nanoTime() method before and after for loops that call the factorial methods 1,000,000 times each for random values.

  2. Write a program that generates four arrays of random int values with lengths 1,000, 10,000, 100,000, and 1,000,000. Make two additional copies of each array. Then, sort each of three copies of the array with the selection sort algorithm given in Example 6.2, the bubble sort algorithm given in Section 10.1, and the merge sort algorithm given in Example 19.8, respectively. Use the System.nanoTime() method to time each of the sorts. Note that both selection sort and bubble sort might take quite a while to sort an array of 1,000,000 elements.

    Run the program several times and find average values for each algorithm on each array size. Plot those times on a graph. The times needed to run selection sort and bubble sort should increase quadratically, but the time to run merge sort should increase linearithmically. In other words, an array length of n should take time proportional to n2 (multiplied by some constant) for selection sort and bubble sort, but it should take time proportional to n log2 n (multiplied by some constant) for merge sort. For large arrays, the difference in time is significant.

  3. Investigate the performance of using recursion to compute Fibonacci numbers. Implement the naive recursive solution, the memoization method, and an iterative solution similar to the memoization method. Use the System.nanoTime() method to time the computations for large values of n.

    Warning: It might take a very long time to compute the nth Fibonacci number with the naive recursive solution.

  4. Exercise 6.9 from Chapter 6 explains how binary search can be used to search for a value in a sorted array of values. The idea is to play a “high-low” game, first looking at the middle value. If the value is the one you’re looking for, you’re done. If it’s too high, look in the lower half of the array. If it’s too low, look in the upper half of the array. Implement binary search both iteratively and recursively. Populate an array with 100,000 int values between 1 and 10,000,000 and sort it. Then, search for 1,000,000 random values generated between 1 and 10,000,000 using iterative binary search and then recursive binary search. Use the System.nanoTime() method to time each process. Was the iterative or recursive approach faster? By how much?

20. File I/O

Kira: What are those funny marks?
Jen: This is all writing.
Kira: What’s writing?
Jen: Words that stay, my master said.

— The Dark Crystal

20.1. Problem: A picture is worth 1,000 bytes

If you’re familiar with bitmap (bmp) files, you know that they can take up a lot of space compared to other popular image formats. People often use a technique called data compression to reduce the size of large files for storage. There are many different kinds of compression and many which are tailored to work well on images. Your task is to write a program that will do a particular kind of compression called run length encoding (RLE), which we’ll test on bitmaps. The idea behind RLE is simple: Imagine a file as a stream of bytes. As you look through the stream, replace repeating sequences of a single byte with a count telling how many times it repeats followed by the byte that repeats. Consider the following sequence.

215 7 7 7 7 7 7 7 7 7 123 94 94 94 71

Its RLE compressed version could be shown as follows.

1 215 9 7 1 123 3 94 1 71

Since there’s no simple way to keep track of which numbers are counts and which ones are actual bytes of data, we have to keep a count for every byte, even unrepeated ones. In this example, we went from 15 down to 10 numbers, a savings of a third. In the worst case, a file that has no repetition at all would actually double in size after being “compressed” with this kind of RLE. Nevertheless, RLE compression is used in practice and performs well in some situations.

Your job is to write a Java program that takes two arguments from the command line. The first is either -c for compress or -d for decompress. The second argument is the name of the file to be compressed or decompressed. When compressing, append the suffix .compress to the end of the file name. When decompressing, remove that suffix.

Executing the following on the command line should generate an RLE compressed file called test.bmp.compress.

java BitmapCompression -c test.bmp

Likewise, executing the following should create an uncompressed file called test.bmp.

java BitmapCompression -d test.bmp.compress

Be sure to make a backup of the original file (in this case test.bmp) because decompressing will overwrite a file of the same name.

To perform the compression, go byte by byte through the file. For each repeating sequence of a byte (which can be as short as a single byte long), write the length of the sequence as a byte and then the byte itself into the compressed file. If a repeating sequence is more than 127 bytes, you break the sequence into more than one piece since the largest value the byte data type can hold in Java is 127. (The byte type in Java is always signed, giving a range of -128 to 127.) Decompression simply reads the count then the byte value and writes the appropriate number of copies of the byte value into the decompressed file.

20.1.1. Command-line arguments

You might be wondering how to read the command-line arguments such as -c test.bmp or -d test.bmp.compress. These arguments can’t be read using Scanner as we read most text. Instead, they’re passed directly into your program. By this point, you’ve written so many main() methods that you might have stopped paying attention to their syntax. Remember that main() methods always take a single parameter of type String[]. We’ve always called this parameter args in this book, but you’re free to call it whatever you like.

This array of String values is how command-line arguments are passed into your program. A Java program invoked by typing java BitmapCompression -c test.bmp will have an array of length 2 passed in. The first String stored in this array (args[0]) will be "-c". The second String stored in this array (args[1]) will be "test.bmp". The operating system passes in these command-line arguments to the JVM which passes them along to your program as String values.

Command-line programs often take these arguments to specify which options the program will be run with. Although they’re useful, we didn’t focus on these arguments in early chapters partly because they involve arrays and partly because all arguments, even those that look like numbers, will be passed in as String values. Furthermore, it’s cumbersome to specify command-line arguments when using IDEs like Eclipse or IntelliJ.

20.2. Concepts: File I/O

Before you can tackle the problem of compressing, or even reading from and writing to, files, some background on files is necessary. By now, you’ve had many experiences with files: editing Java source files, compiling those files, and running them on a virtual machine, at the very least. You’ve probably done some word processing, looked at photos, listened to music, and watched videos, all on a computer. Each of these activities centers on one or more files. In order to reach these files, you probably had to look through some directories, which are a special kind of file as well. But what is a file?

20.2.1. Non-volatile storage

A computer program is like a living thing, always moving and restless. The variables in a program are stored in RAM, which is volatile storage. The data in volatile memory will only persist as long as there’s electricity powering it. But most programs don’t run constantly, and neither do most computers. We need a place to store data between runs of a particular program. Likewise, we need a place to store data when our computer isn’t turned on. Both scenarios share a common solution: secondary storage such as hard drives, flash drives, and optical media like DVD and Blu-ray discs.

Files are not always stored in non-volatile memory. It’s possible to load entire files into RAM and keep them there for long periods of time. Likewise, all input and output on Unix and Linux systems are viewed as file operations. Nevertheless, the characteristics of non-volatile memory are often associated with file I/O: slow access times and the possibility of errors in the event of inaccessible files or hardware problems.

20.2.2. Stream model

While discussing RLE encoding, we described a file as a stream of bytes, and that’s a good definition for a file, especially in Java. Since Java is platform independent, and different operating systems and hardware will deal with the nitty gritty details of storing files in different ways, we want to think about files as abstractly as possible. Reading from and writing to a stream of bytes is not so different from the other input and output you’ve done so far. For the most part, file I/O will be similar to command-line I/O and, in fact, can use some of the same classes.

Although reading and writing from the files can be like reading from the keyboard and writing to the screen, there are a few additional complications. For one thing, you must open a file before you can read or write. Sometimes opening the file will fail: You could try to open a file for reading which doesn’t exist or try to open a file for writing which you don’t have permissions for. When reading data, you might try to read past the end of the file or try to read an int when the next item is a String. Unlike reading from the keyboard, you cannot ask the user to try again if there’s a mistake in input. To deal with these possible errors, exception handling will accompany many different file I/O operations in Java.

20.2.3. Text files and binary files

When talking about files, many people divide files into two categories: text files and binary files. A text file can be read by humans. That is, when you open a text file with a simple file reader, it won’t be filled with gibberish and nonsense characters. A Java source file is an excellent example of a text file.

In contrast, a binary file is a file meant only to be read by a computer. Instead of printing out characters meant to be read by a human, the raw bytes of memory for specific pieces of data are written to binary files. To clarify, if we wanted to write the number 2,127,480,645 in a text file, the file would contain the following.

2127480645

However, if we wanted to write the same number in a binary file, the file would contain the following.

~ÎÇE

If you recall, an int in Java uses four bytes of storage. There’s a system of encoding called the ASCII table which maps each of the 256 (0–255) numerical bytes to a character. The four characters given above are the extended ASCII representation of the four bytes of the number 2,127,480,645.

In some sense, the idea of a text file is artificial. All files are binary in the sense that they’re readable by a computer. You’ll take different steps and create different objects depending on whether you want to do file I/O in the text or binary paradigms, but the overall process will be similar in either case.

20.3. Syntax: File operations in Java

20.3.1. The File class

A basic class for interacting with files in Java is the File class. A File object allows you to interact with a file at the operating system level. You can create a new file, test to see if a file is a directory, find out the size of a file, and so on. A number of file I/O classes require a File object as a parameter. To use the File class, import java.io.File or java.io.*.

To create a File object, you can call the File constructor with a String specifying the name of the file.

File file = new File("file.txt");

Doing so will create a virtual file object associated with the name file.txt (which might not exist yet) in the working directory of the Java program. In this case, the extension txt doesn’t have any real meaning. On many systems, the extension (like pdf or html) is used by the operating system to guess which application should open the file. To Java, however, the extension is just part of the file name. A file name passed to the constructor of a File object can have any number of periods in it (or none).

A file name without a directory is all well and good, but file systems are useful in part because of their hierarchical structure. If we want to create a file in a particular location, we specify the path in the String before the name of the file.

File file = new File("/homes/owilde/documents/file.txt");

In this case, the prefix /homes/owilde/documents/ is the path and file.txt is still the file name. Each slash ('/') separates a parent directory from the files or directories inside of it. This path specifies that we start at the root, go into the homes directory, then the owilde directory, and then the documents directory. Note that we can also use a single period (.) in a path to refer to the current working directory and two periods (..) to refer to a parent directory.

This is one of those sticky places where Java’s trying to be platform independent, but the platforms each have different needs. The example we gave above is for a macOS or Linux system. In Windows, the way to specify the path is slightly different. Creating a similar File object on Windows system might be done as follows.

File file = new File("C:\\Users\\owilde\\Documents\\file.txt");

Then, the path specifies that we start in the C drive, go into the Users directory, the owilde directory, and then the Documents directory. Windows systems use a backslash (\) to separate a parent directory from its children. But in Java a backslash isn’t allowed to be by itself in a string literal, and so each backslash must be escaped with another backslash. To simplify things somewhat, Java allows Windows paths to be separated with regular slashes as well, so we’ll use this style for the rest of the book.

A further complication is that file and directory names are case sensitive in Linux, aren’t case sensitive in Windows, and could be either in macOS depending on file system settings.

Returning to File objects, there are a number of things we can do directly. A File object has methods that can test if a file with the associated name and path exists, if it’s readable, if it’s writable, and many other things. Because there are so many classes associated with file I/O and each class has so many methods, now’s a good time to remind you of the usefulness of the Java API. If you visit the Java API documentation site, you can get detailed documentation for the entire standard library, including file I/O classes.

20.3.2. Reading and writing text files

Once you have a File object, most of its usefulness comes from combining it with other classes. You’re already familiar with the Scanner class. The Scanner constructor can take a File object (instead of System.in), creating a Scanner that reads from a text file instead of the keyboard.

Scanner in = null;
try {
    in = new Scanner(file);
    while(in.hasNextInt())
        process(in.nextInt());
}
catch(FileNotFoundException e) {
    System.out.println("File " + file.getName() + " not found!");
}
finally { if(in != null) in.close(); }

Assuming that file is linked to a file which the program has read access to, this block of code will extract int values from the file and pass them to the process() method. If the file doesn’t exist or isn’t readable to the program, a FileNotFoundException will be thrown and an error message printed. Creating a Scanner from a File object instead of System.in can throw a checked exception, so the try and catch are needed before the program will compile. Note that you’ll need to import java.util.Scanner or java.util.* just like any other time you use the Scanner class.

And that’s all there is to it. After opening the file, using the Scanner class will be almost the same as before. One difference is that you should close the Scanner object (and by extension the file) when you’re done reading from it, as we do in the example. Closing files is key to writing robust code.

You’ll notice that we put in.close() in a finally block. Using finally is a good habit for file I/O. File operations could fail for any number of reasons, but you will still need to close the file afterward. We put in the null check in case the file didn’t exist and the reference in never pointed to a valid object. (We also begin by setting in to null. Otherwise, Java complains that it might not have been initialized.)

Writing information to a file is similar to using System.out. First, you need to create a PrintWriter object. Like Scanner, you can create a PrintWriter object directly from a File object. If we want to write a list of random numbers to the file we were reading from earlier, we could do it as follows.

PrintWriter out = null;
try {
    out = new PrintWriter(file);
    Random random = new Random();
    for(int i = 0; i < 100; ++i)
        out.println(random.nextInt());
}
catch(FileNotFoundException e) {
    System.out.println("File " + file.getName() + " not found!");
}
finally { if(out != null) out.close(); }

Again, once you have a PrintWriter object, the methods for outputting data are just like using System.out. Be sure to import java.io.* in order to have access to the PrintWriter class.

20.3.3. Reading and writing binary files

We covered text files first because their input and output is similar to command-line I/O. When reading and writing text files, you can visually verify that file reading and writing operations were successful. Although it’s harder to check the contents of binary files, they have other advantages. Data can often be stored more compactly in binary files, as in the example with the integer 2,127,480,645. Even better, Java provides facilities for easily dumping (and later retrieving) primitive data types, objects, and even complex data structures to binary files.

The simplest object for reading input from a binary file is a FileInputStream object. As with a File object, you can create a FileInputStream object from a String specifying the file path and name.

FileInputStream in = new FileInputStream("file.bin");

Unfortunately, you can’t do much with a FileInputStream object. Its methods allow you to read single bytes, either one at a time or into an array as a group. The basic read() method returns the next byte in the file or a -1 if the end of the file has been reached. Working only at the level of bytes, we can still write useful code like the following method that prints the size of a file.

public static void printFileSize(String fileName) {
    FileInputStream in = null;
    try {
        in = new FileInputStream(fileName);
        int count = 0;
        while(in.read() != -1)
            count++;
        System.out.println("File size: " + count + " bytes");
    }
    catch(Exception e) {
        System.out.println("File access failed.");
    }
    finally { try{ in.close(); } catch(Exception e){} }
}

Note the extra try-catch block inside of the finally. Like the other binary file I/O objects we’ll discuss in this section, FileInputStream can throw an IOException when closing. Usually, you won’t need to deal with this exception, but you still must catch it. Since we’re catching any Exception, we’re saving a little bit of code by eliminating the null check. If in is null in this example, a NullPointerException will be thrown and immediately caught, causing no damage. Catching Exception breaks our rule of never catching exceptions that are broader than we need, but we only have a single line of code in our try block with nothing else to do if it fails.

To output a sequence of bytes, you can create a FileOutputStream object. Its write() methods are the mirror images of the read() methods in FileInputStream. It would be convenient if there was a way to read and write any primitive type instead of just byte values, and DataInputStream and DataOutputStream provide exactly that functionality.

For output, a DataOutputStream chops up primitive data types into their component bytes and sends those bytes to a FileOutputStream. For input, a DataInputStream reads a sequence of bytes from a FileInputStream and reassembles them into whatever kind of primitive data they’re supposed to be.

To create an DataInputStream, you supply a FileInputStream to its constructor, usually one that you’ve just created on the fly for this purpose.

DataInputStream in = new DataInputStream(new FileInputStream("baseball.bin"));

Now, let’s assume that baseball.bin contains baseball statistics. The first thing in the file is an int indicating the number of records it contains. Then, for each record, it’ll list home runs, RBI, and batting average, as an int, an int, and a double, respectively. Assuming that we’ve opened the file correctly above, we can read these statistics into three arrays with the following code.

DataInputStream in = null;
try {
	in = new DataInputStream(new FileInputStream("baseball.bin"));
	int records = in.readInt();
	int[] homeRuns = new int[records];
	int[] RBI = new int[records];
	double[] battingAverage = new double[records];
	for(int i = 0; i < records; ++i) {
		homeRuns[i] = in.readInt();
		RBI[i] = in.readInt();
		battingAverage[i] = in.readDouble();
	}
}
catch(IOException e) {
    System.out.println("File reading failed.");
}
finally { try{ out.close(); } catch(Exception e){} }

When opening the file in the FileInputStream constructor, a FileNotFoundException will be thrown if the file doesn’t exist or is inaccessible. If the readInt() or readDouble() methods fail, they’ll throw an IOException. If the DataInputStream object tries to read past the end of a file, it’ll throw an EOFException exception. If you wanted to deal with these exceptions separately, you could, but since FileNotFoundException and EOFException are both children of IOException, a single catch clause for IOException handles all three.

As expected, the DataOutputStream methods for writing to a file match DataInputStream methods for reading from a file. If you substitute write for read, DataOutputStream methods are almost the same as DataInputStream methods. Below is a companion piece of code which assumes that homeRuns, RBI, and battingAverage are filled with data and writes them to a file.

DataOutputStream out = null;
try {
    out = new DataOutputStream(new FileOutputStream("baseball.bin"));
    out.writeInt(homeRuns.length);
    for(int i = 0; i < homeRuns.length; ++i) {
        out.writeInt(homeRuns[i]);
        out.writeInt(RBI[i]);
        out.writeDouble(battingAverage[i]);
    }
}
catch(IOException e) {
    System.out.println("File writing failed.");
}
finally { try{ out.close(); } catch(Exception e){} }

Using DataInputStream and DataOutputStream in this way isn’t too difficult, but it seems cumbersome. The programmer has the responsibility to read and write every piece of primitive data separately. It would be convenient if there was a way to read an entire object at once, including any references to other objects that it contains. If a tool exists for reading an entire object, we’d also want a matching tool for writing an entire object at once.

Such tools can be found in the ObjectInputStream and ObjectOutputStream classes, respectively. These file I/O objects provide methods that elegantly allow you to read or write a whole object at a time. To use them with our baseball data example, we need to define a new class.

Program 20.1 Serializable BaseballPlayer class.
import java.io.Serializable;

public class BaseballPlayer implements Serializable {
    private int homeRuns;
    private int RBI;
    private double battingAverage;
    
    public BaseballPlayer(int homeRuns, int RBI, double battingAverage) {
        this.homeRuns = homeRuns;
        this.RBI = RBI;
        this.battingAverage = battingAverage;
    }
    
    public int getHomeRuns() { return homeRuns; } 
    public int getRBI() { return RBI; }
    public double getBattingAverage() { return battingAverage; }
}

The new class BaseballPlayer encapsulates the three pieces of information we want. Note that it also implements the interface Serializable, but it doesn’t seem to implement any special methods to conform to the interface. We’ll discuss this interface more after we show how using this new class can simplify file I/O. Our input code will change to the following.

ObjectInputStream in = null;
try {
    in = new ObjectInputStream(new FileInputStream("baseball.bin"));
    int records = in.readInt();
    BaseballPlayer[] players = new BaseballPlayer[records];
    for(int i = 0; i < players.length; ++i)
        players[i] = (BaseballPlayer)in.readObject();
}
catch(IOException e) {
    System.out.println("File reading failed.");
}
finally { try{ in.close(); } catch(Exception e){} }

The corresponding output will become what follows.

ObjectOutputStream out = null;
try {
    out = new ObjectOutputStream(new FileOutputStream("baseball.bin"));
    out.writeInt(players.length);
    for(int i = 0; i < players.length; ++i)
        out.writeObject(players[i]);
}
catch(IOException e) {
    System.out.println("File writing failed.");
}
finally { try{ out.close(); } catch(Exception e){} }

This process of outputting an entire object at a time is called serialization. The BaseballPlayer class is very simple, but even complex objects can be serialized, and Java takes care of almost everything for you. The only magic needed is for the class that’s going to be serialized to implement Serializable. There are no methods in Serializable. It’s just a tag for a class that can be packed up and stored. The catch is that, if there are any references to other objects inside of the object being serialized, they must also be serializable. Otherwise, a NotSerializableException will be thrown when the JVM tries to perform the serialized output. Many classes are serializable, including the vast majority of the Java API.

However, objects that have some kind of special system-dependent state, like a Thread or a FileInputStream object, can’t be serialized. If you need to serialize a class with references to objects like these, add the transient keyword to the declaration of each unserializable reference. That said, these should be few and far between. For BaseballPlayer, adding implements Serializable was all we needed, and we can still get more mileage out of serialization! An array can be treated liked an Object and is also serializable. We can further simplify the input as shown below.

ObjectInputStream in = null;
try {
    in = new ObjectInputStream(new FileInputStream("baseball.bin"));
    BaseballPlayer[] players = (BaseballPlayer[])in.readObject();
}
catch(IOException e) {
    System.out.println("File reading failed.");
}
finally { try{ in.close(); } catch(Exception e){} }

And the corresponding output code can be simplified as well.

ObjectOutputStream out = null;
try {
    out = new ObjectOutputStream(new FileOutputStream("baseball.bin"));
    out.writeObject(players);
}
catch(IOException e) {
    System.out.println("File writing failed.");
}
finally { try{ out.close(); } catch(Exception e){} }

When you go to write your code, which binary file I/O classes should you use? It depends on the situation. FileInputStream and FileOutputStream are very low level. You’ll use those classes to construct DataInputStream, DataOutputStream, ObjectInputStream, and ObjectOutputStream objects, but you probably won’t use them on their own unless your application is focused on byte-level input and output.

If you only want to read and write primitive types, you can use DataInputStream and DataOutputStream objects. These classes have methods to read and write all primitive types. Ultimately, all objects are made up of primitive types, though those primitive types might be buried inside of other objects. Most languages provide binary I/O tools like DataInputStream and DataOutputStream, so code using these objects will be similar to code in other languages that writes individual pieces of primitive data.

Finally, if you want to read and write whole objects (or even arrays of objects) at a time, ObjectInputStream and ObjectOutputStream are powerful tools. Using them leverages Java serialization, making the JVM do the work of dividing up your objects into primitive types and writing them (for output) or reading that primitive data and reassembling it into objects (for input). Even complex objects with many (potentially circular) references to other objects, like linked list classes, can be serialized. The Java implementation of serialization is smart enough to write each unique object only once and then refer to it later. This power feels like magic, but serialization has limitations. Only classes that implement the Serializable interface can be serialized. Also, serialization carries with it some overhead: the files must contain additional metadata describing the class being stored. Using DataInputStream and DataOutputStream can allow you to write only the necessary member data, resulting in a smaller file. You can also run into trouble if you make changes to a class. If one version of an object is serialized and then you try to read that object back after making the smallest change to the class, your code will fail. One last issue is that Java serializes objects according to its own rules, making files written in this way difficult (if not impossible) to use with code written in other languages. This consideration is not insignificant, since files are often written by one program and read by another.

The following table summarizes the three approaches to binary I/O we’ve discussed. Be sure to consult each class’s documentation for more information.

Class Use Purpose Limitations

FileInputStream

Input

Simplest binary I/O

Can only read and write byte values

FileOutputStream

Output

DataInputStream

Input

Binary I/O for primitive types

Can’t read and write whole objects

DataOutputStream

Output

ObjectInputStream

Input

Powerful binary I/O for all types

Depends on Java serialization and can’t work with files created in other ways

ObjectOutputStream

Output

20.3.4. Using JFileChooser

One of the more tedious aspects of working with files in command-line programs is typing the name of the file correctly. Although most OS terminals have time-saving autocomplete features to help users choose the right file name, typing the name of a file directly into a prompt so that it can be read by a Scanner object is error prone. Navigating long directory paths can also be a headache when using a command-line interface.

Indeed, most users are used to selecting files to open or to save via a GUI file chooser instead of typing file names explicitly. The Java Swing library provides such a file chooser called JFileChooser.

We discussed fully featured GUI programs in Chapter 15, but like the JOptionPane class covered in Chapter 7, JFileChooser can be used with or without a complex GUI.

Unlike the JOptionPane class whose functionality is accessed through static methods, you must create a JFileChooser object to use it.

JFileChooser chooser = new JFileChooser();

Once you’ve created the JFileChooser object, you can call either the showOpenDialog() method to show a dialog to open existing files or the showSaveDialog() to save a potentially new file. The dialog looks similar in either case, with only minor differences such as title and buttons names.

int result = chooser.showOpenDialog(null);

Both methods take a Component object as an argument. If you’re creating a GUI program, you can pass in a JFrame or a JDialog for this argument to pop up a modal file chooser dialog that must be dealt with before returning control to the parent frame or dialog. If your program doesn’t otherwise use a GUI, you can pass in null.

Both methods also return an int value indicating the result of user input. A return value of JFileChooser.APPROVE_OPTION means that the user selected a file. The value JFileChooser.CANCEL_OPTION means that the user canceled instead of picking a file. Finally, JFileChooser.ERROR_OPTION means some error occurred.

Once the user has selected a file for opening or for saving, you can call the getSelectedFile() method to retrieve the File that the user selected.

File file = chooser.getSelectedFile();

If the user canceled or an error occurred, this File object could be null.

In many cases, a programmer will want to focus the user on files of a certain kind. For example, a program that plays audio files might display only files that have extensions associated with audio formats such as wav, mp3, and flac. To include this functionality, you can create a FileNameExtensionFilter object and set it as a file filter on your JFileChooser.

FileNameExtensionFilter filter = new FileNameExtensionFilter("Audio", "wav", "mp3", "flac");
chooser.setFileFilter(filter);

The first argument to the FileNameExtensionFilter constructor is a user-friendly description of the kinds of files displayed by the filter. After the description, the constructor takes a variable number of arguments, each of which gives one of the included file extensions. Extensions are case insensitive and should not include a dot (.) at the beginning. You should set the file filter before displaying a dialog with either showOpenDialog() or showSaveDialog().

FileNameExtensionFilter covers most of what people want from a file filter, but it’s possible to create your own class that extends FileFilter if you need to filter files based on more complex criteria.

Next, we’ll give a short example that uses a JFileChooser.

Example 20.1 Using JFileChooser

The short program below allows a user to select a file using JFileChooser. Only image files with jpg or png extensions will be displayed. Once the file has been selected, the program will print out the number of bytes of storage that the file uses.

Program 20.2 Tool to determine size of file selected with JFileChooser.
import javax.swing.JFileChooser; (1)
import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.File;

public class FileChooserExample {
	public static void main(String[] args) {
		JFileChooser chooser = new JFileChooser();  (2)
		FileNameExtensionFilter filter = new FileNameExtensionFilter("Images", "jpg", "png");
		chooser.setFileFilter(filter); (3)
		
		int result = chooser.showOpenDialog(null);  (4)
		if(result == JFileChooser.APPROVE_OPTION) { (5)
			File file = chooser.getSelectedFile();
			if(file != null && file.exists()) {		(6)
				long size = file.length();
				System.out.println("The file contains " + size + " bytes.");
			}
			else
				System.out.println("The file doesn't exist.");
		}
		else
			System.out.println("The user probably canceled.");
	}
}
1 Imports are needed for the JFileChooser, the FileNameExtensionFilter, and the File object itself.
2 First, we create the JFileChooser object.
3 Next, we create and then set a file filter appropriate for image files.
4 We show the open dialog to allow the user to select a file.
5 If the user selected a file rather than canceling, we get the File object from the file chooser.
6 If the file exists, we get its length in bytes and print that out.

If the file didn’t exist or the user hit cancel, we print appropriate messages in those cases as well.

Figure 20.1 shows the dialog displayed by this program.

fileChooserFigure
Figure 20.1 JFileChooser showing an open dialog.

Note that the purpose of JFileChooser is to allow users to select a file. It doesn’t guarantee that the file exists or that the user has rights to read from the file or to write to it. Most commercial software asks the user if he or she wants to overwrite an existing file when saving. JFileChooser doesn’t have that functionality built in, requiring additional program logic to to prompt the user if an existing file is about to be overwritten.

Like most of the Swing library, JFileChooser has many options and features that we don’t have time to cover. Its display can be customized, and it can be configured to interact with the file system in a number of ways, such as displaying only normal files, displaying only directories, or both. There are even settings that allow the user to select multiple files at once.

20.4. Examples: File examples

Example 20.2 Directory listing

Let’s return to the File class and look at another example of how to use it. It’s often useful to know the contents of a directory. At the Windows command prompt, this is usually done using the dir command; in Linux and macOS, the ls command is generally used. In a few lines of code, we can write a directory listing tool which lists all the files in a directory, the date each file was last modified, and whether or not a file is a directory.

Program 20.3 Directory listing tool.
import java.io.*;
import java.text.*;
import java.util.*;

public class Directory {    
    public static void main(String[] args) {
        File directory = new File(".");			(1)
        File[] files = directory.listFiles();	(2)
        for(File file: files) {
            System.out.print(DateFormat.getDateInstance().format(
                new Date(file.lastModified())) + "\t");	(3)
            if(file.isDirectory())						(4)
                System.out.print("directory");			
            else
                System.out.print("\t");
            System.out.println("\t" +  file.getName());	(5)
        }               
    }
}
1 The code first creates a File object using "." to specify the current working directory.
2 The listFiles() method returns an array of File objects which we then iterate over.
3 We call lastModified() on each file to get its date,
4 isDirectory() to see if it’s a directory,
5 and finally print the name given by getName().

The output for this program might look like the following.

Aug 5, 2019                     AreaFromRadiusBinary.java
Aug 1, 2019                     AreaFromRadiusText.java
Aug 5, 2019                     BaseballPlayer.java
Aug 5, 2019                     BitmapCompression.java
Aug 5, 2019                     ConcurrentFileAccess.java
Aug 8, 2019                     Directory.class
Aug 8, 2019                     Directory.java
Aug 1, 2019                     FileChooserExample.java
Aug 8, 2019     directory       Images
Aug 5, 2019                     areas.bin
Aug 5, 2019                     areas.txt
Aug 5, 2019                     radiuses.bin
Aug 5, 2019                     radiuses.txt
Example 20.3 Radiuses stored in a file

Now, let’s look at a data processing application of files. Let’s assume that there’s a file called radiuses.txt which holds the radiuses of a number of circles formatted as text, one on each line of the file. It’s our job to read each radius r, compute the areas of each circle using the formula Area = Ï€r2, and write those areas to a file called areas.txt.

Program 20.4 Reads a list of radiuses from a text file and outputs their areas to another text file.
import java.io.*;
import java.util.*;

public class AreaFromRadiusText {
    public static void main(String[] args) {
        File inFile = new File("radiuses.txt");
        File outFile = new File("areas.txt");
        Scanner in = null;
        PrintWriter out = null;     
        try {
            in = new Scanner(inFile);		(1)
            out = new PrintWriter(outFile);	(2)
            while(in.hasNextDouble()) {		(3)
                double radius = in.nextDouble();				(4)
                out.format("%.3f%n", Math.PI*radius*radius);	(5)
            }
        }
        catch(FileNotFoundException e) { 	(6)
            System.out.println(e.getMessage());
        }
        finally { 							(7)
            if(in != null) in.close();
            if(out != null) out.close();
        }
    }
}
1 First, we create a Scanner to read text from a file.
2 Next, we create a PrintWriter to write text to a file.
3 We continue reading as long as there’s another piece of text formatted as a legal double.
4 We read in the double, just as we would from a user typing on the keyboard.
5 We use the format() method to print out a double formatted with exactly three digits after the decimal point followed by a newline, just as we’ve used System.out.format() in the past.
6 As is typical with file I/O, we have to catch exceptions. A FileNotFoundException would have been thrown if either file was inaccessible.
7 In the finally block, we close both files. It’s vitally important to close output files to ensure that all their data is actually written to disk.

Perhaps the input file radiuses.txt contains the following 10 values.

33.675
4.156
8.608
60.350
86.501
78.581
23.935
2.263
26.827
73.358

Then, the output file areas.txt would be filled with these corresponding 10 values. Note that formatting the output to have three digits after the decimal point is easier to read, but it loses some precision.

3562.584
54.263
232.785
11442.065
23506.725
19399.252
1799.769
16.089
2260.966
16906.155

The previous class did all of its input and output with text files. We’ll also implement this program to read from a binary file called radiuses.bin and write to a binary file called areas.bin.

Program 20.5 Reads a list of radiuses from a binary file and output their areas to another binary file.
import java.io.*;

public class AreaFromRadiusBinary {
    public static void main(String[] args) {
        File inFile = new File("radiuses.bin");
        File outFile = new File("areas.bin");
        DataInputStream in = null;
        DataOutputStream out = null;        
        try {
            in = new DataInputStream(new FileInputStream(inFile));		(1)
            out = new DataOutputStream(new FileOutputStream(outFile));	(2)
            while(true) {	(3)
                double radius = in.readDouble();
                out.writeDouble(Math.PI*radius*radius);              
            }           
        }
		catch(EOFException e) {} // End of file reached (4)
        catch(IOException e) {
            System.out.println(e.getMessage());
        }
        finally { 
            try { in.close(); } catch(Exception e){}
            try { out.close(); } catch(Exception e){}
        }
    }
}
1 We change the Scanner from the text version of this program to a DataInputStream.
2 We change the PrintWriter to a DataOutputStream.
3 We make what appears to be the strange choice of changing the while loop to an infinite loop. We do this because the easiest way to see if there’s any more data in a binary file is to keep reading until an EOFException is thrown.
4 When the EOFException is thrown, we do nothing to handle it because, in this case, it’s just a signal to stop reading.

20.5. Solution: A picture is worth 1,000 bytes

Now, we’ll give the solution to the problem posed at the beginning of the chapter. First, let’s look at the class definition and main() method.

import java.io.*;

public class BitmapCompression {
    public static void main(String[] args) {        
		if(args.length != 2)					(1)
			System.out.println("Usage: java BitmapCompression (-c|-d) file");
		else {
			DataInputStream in = null;
			try {           
				in = new DataInputStream(new FileInputStream(args[1])); (2)
				if(args[0].equals("-c"))		(3)
					compress(in, args[1]);
				else if(args[0].equals("-d"))
					decompress(in, args[1]);          
			}
			catch(FileNotFoundException e) {	(4)
				System.out.println("File not found: " + e.getMessage());
			}
			finally { try{ in.close(); } catch(Exception e){} } (5)
		}
    }
1 We first check that there are exactly two command-line arguments. Otherwise, our program will crash when we try to access invalid array locations. For the wrong number of arguments, we print a usage message.
2 If we have the right number of arguments, we open a DataInputStream based on the file named passed as the second command-line parameter.
3 Then, we either compress or decompress the file depending on which switch was passed as the first command-line parameter.
4 The catch will deal with the FileNotFoundException by printing an appropriate error message.
5 Note the usual finally where the file is closed, including the inner try-catch blocks needed to safely close binary files.
    public static void compress(DataInputStream in, String file) {
        DataOutputStream out = null;
        try {
			String compressed = file + ".compress";
            out = new DataOutputStream(new FileOutputStream(compressed)); (1)
			byte current = 0;
			int count = 1;
			try {
				current = in.readByte(); 		(2)
				while(true) {
					byte temp = in.readByte(); 	(3)
					if(temp == current && count < 127)
						count++;				(4)
					else {						(5)
						out.writeByte(count);
						out.writeByte(current);               
						count = 1;
						current = temp;
					}
				}
			}
			catch(EOFException e) { // Last sequence of bytes (6)
				out.writeByte(count);
				out.writeByte(current);
			}			
        }		
        catch(IOException e) {	(7)
			System.out.println("Compression failed: " + e.getMessage());
        }
        finally { try { out.close(); } catch(Exception e){} }
    }
1 In the compress() method, we first open a new DataOutputStream for a file named the same as the input file with .compress tacked on the end.
2 We read in the first byte of data.
3 Then, we keep reading bytes of data from the input file.
4 As long as we keep seeing the same byte, we increment a counter.
5 When we run into a new byte (or when we reach the limit of 127 of the same consecutive byte), we write the count and the byte we’ve been reading and move on, resetting the counter.
6 When an EOFException is thrown, we’ve reached the end of the file. Because of the way our code is structured, we’ll always have at least one byte (and possibly a long sequence of matching bytes) that we haven’t yet written to the file. Consequently, we have to write the counter and the current byte to finish.
7 The method finishes with the usual catch and finally blocks needed to catch errors and safely close the output file.
    public static void decompress(DataInputStream in, String file) {
        DataOutputStream out = null;
        try {
			String original = file.substring(0, file.lastIndexOf(".compress")); (1)
            out = new DataOutputStream(new FileOutputStream(original)); (2)
            while(true) {            
				int count = in.readByte(); (3)
                byte temp = in.readByte();           
                for(int i = 0; i < count; i++)
                    out.writeByte(temp);                          
            }
        }
		catch(EOFException e) {} // Input finished (4)
        catch(IOException e) {	(5)
			System.out.println("Decompression failed: " + e.getMessage());
        }               
        finally { try{ out.close(); } catch(Exception e){} }
    }
}
1 The decompress() method is simpler than compress(). It begins by finding the original name of the file by removing .compress. Note that this code will crash if the file being decompressed doesn’t end with .compress.
2 Then, we open a new DataOutputStream for a file with the original name we’ve found.
3 Next, we read a counter, read a byte value, and write the byte value as many times as the count specifies.
4 As before, an EOFException signals the end of the input file.
5 An additional catch and finally deal with errors and close the output file.

20.6. Concurrency: File I/O

By now, you’ve seen threads behave in unpredictable ways because of the way they’re reading and writing to shared variables. Isn’t a file a shared resource as well? What happens when two threads try to access a file at the same time? If both threads are reading from the file, everything should work fine. If the threads are both writing or doing a combination of reading and writing, there can be problems.

As we mentioned in Section 20.3, file operations are OS dependent. Although Java tries to give a uniform interface, different system calls are happening at a low level. Consequently, the results may be different as well.

Consider the following program that spawns two threads that both print a series of numbers to a file called concurrent.out. The first thread prints the even numbers between 0 and 9,999 while the second thread prints the odd ones.

Program 20.6 Spawns threads that print odd and even numbers to a file concurrently.
import java.io.*;

public class ConcurrentFileAccess implements Runnable {
    private boolean even;
    
    public static void main(String args[]) {
        Thread writer1 = new Thread(new ConcurrentFileAccess(true)); (1)
        Thread writer2 = new Thread(new ConcurrentFileAccess(false));     
        writer1.start(); (2)
        writer2.start();
    }
    
    public ConcurrentFileAccess(boolean even) {
        this.even = even;
    }
    
    public void run() {
        PrintWriter out = null;
        int start = even ? 0 : 1; (3)
        try {
			out = new PrintWriter(new File("concurrent.out")); (4)
            for(int i = start; i < 10000; i += 2) (5)
                out.println(i);				
        }
        catch (FileNotFoundException e) {
            System.out.println("concurrent.out not found!");
        }
        finally { if(out != null) out.close(); } (6)
    }   
}
1 The code in this program should have few surprises. The main() method creates two Thread objects from ConcurrentFileAccess objects, each with a different value for its even field.
2 Then, the main() method starts the threads running.
3 In each thread’s run() method, it first decides on a starting point of 0 or 1 depending on whether it’s supposed to be even or odd. Note the use of the ternary operator.
4 Each thread opens the file concurrent.out.
5 Then, it starts printing out even or odd numbers, depending on its starting point.
6 Afterward, each thread closes the file and ends.

What do you expect the file concurrent.out to look like after the program’s completed? Run it several times, on Windows, Linux, and macOS systems if you can. The file might contain runs switching back and forth between even numbers and odd numbers, but it’s likely that many numbers, often about half, will be missing.

Why are half the numbers getting lost? When you open a file for writing, it overwrites the contents of the file. Thus, entire sequences of numbers are getting saved and then lost. We can change this behavior by changing the line below.

out = new PrintWriter(new File("concurrent.out"));

We replace it with the following.

out = new PrintWriter(new FileOutputStream("concurrent.out", true));

The PrintWriter constructor that takes a File object actually calls another constructor internally that builds an output stream object. Instead, we can pass in a FileOutputStream object created with a second boolean parameter set to true. Doing so creates a FileOutputStream stream (and consequently a PrintWriter) whose output will be appended to the file instead of overwriting it.

After this change, what does the file look like when we run the program? Since we’re going to append to the file instead of overwriting, make sure that you delete concurrent.out before running the program again. As usual, the file might look different on different systems. The file probably contains long runs of numbers from each thread. In fact, it’s possible to have the complete output from one thread followed by the complete output from the other.

For performance reasons, file operations are usually done in batches. Instead of writing each number to the file as the thread produces it, output is usually stored in a buffer which is written as a whole. By calling out.flush() after each out.println() call, we could flush the buffer to the file after each number is generated. Doing so won’t be as efficient, but it may give us some insight into how concurrent writes on files work.

Using flushes, the output from the two threads should be thoroughly intermixed. On a Windows machine, if you copy the data from the file and sort it, it’s possible that you’ll see some numbers missing. This lost output is similar to situations where updates to variables were lost because they were overwritten by another thread. On the other hand, most Linux systems have better concurrent file writing and won’t lose any numbers. (Even on Linux, it’s possible for a number to be printed in the middle of another number, but no digits should be lost.)

Under ideal circumstances, no two threads or processes should be writing to the same file. However, this situation is sometimes unavoidable, as with a database program that must support concurrent writes for the sake of speed. If you need to enforce file locking, you can prevent threads within your own program from accessing a file concurrently by using normal Java synchronization tools. If you expect other programs to interact with the same files that your program will use, Java provides a FileLock class which allows the user to lock up portions of a file. As with everything file related, FileLock is dependent on the underlying OS and might behave differently on different systems. Also, FileLock is a tool from the Java Non-blocking I/O (NIO) library, which provides many impressive features but views I/O in a significantly different way from the file I/O operations discussed in this chapter.

20.7. Exercises

Conceptual Problems

  1. What’s the difference between volatile and non-volatile memory? Which is usually associated with files and why?

  2. What’s the difference between text and binary files? What are the pros and cons of using each?

  3. We can define compression ratio to be the size of the uncompressed data in bytes divided by the size of the compressed data in bytes. What’s the theoretical maximum compression ratio you could get out of the RLE encoding we used? What’s the theoretical lowest compression ratio you could get out of the RLE encoding we used?

  4. What’s serialization in Java? What do you have to do to serialize an object?

  5. What kinds of objects can’t be serialized?

Programming Practice

  1. Write methods with the following signatures.

    1. public static int readInt(FileInputStream in)

    2. public static long readLong(FileInputStream in)

    3. public static short readShort(FileInputStream in)

    In each case, the method should read the appropriate number of bytes (4 for int, 8 for long, and 2 for short) using the FileInputStream object and reassemble those bytes into the integer type specified. Your methods should be compatible with integers written by a DataOutputStream object. Note that such data is written in big-endian format. In other words, the first byte of data corresponds to the most significant byte in an integer, the second byte of data corresponds to the second-most significant byte, and so on.

  2. Program 20.5 from Example 20.3 computes the areas of circles whose radiuses are given as double values stored in a binary file. Although we provided a sample text file, we didn’t show a sample binary file since the contents would look like gibberish. Write a program that reads a file filled with double values stored as text and writes those same values into another file, storing the double values in binary. Afterward, you should be able to convert our sample text file into a sample binary file that can be used with Program 20.5.

  3. Re-implement the RLE bitmap compression program from Section 20.5 using only FileInputStream and FileOutputStream for file input and output. In some ways, doing so is simpler since you only need byte input and output for this program.

  4. Update the RLE bitmap compression program from Section 20.5 to use JFileChooser to allow the user to select a file with a GUI instead of using command-line arguments.

  5. Re-implement the maze solving program from Section 19.5 to ask the user for a file instead of reading from standard input.

  6. An HTML file contains many tags such as <p>, which marks the beginning of a paragraph, and </p>, which marks the end of a paragraph. A lesser known feature of HTML is that ampersand (&) can mark special HTML entities used to produce symbols on a web page. For example, &pi; is the entity for the Greek letter Ï€. Because of these features of the language, raw text that’s going to be marked up in HTML should not contain less than signs (<), greater than signs (>), or ampersands (&).

    Write a program that reads in an input text file specified by the user and writes to an output text file also specified by the user. The output file should be exactly the same as the input file except that every occurrence of a less than sign should be replaced by &lt;, every occurrence of a greater than sign should be replaced by &gt;, and every occurrence of an ampersand should be replaced by &amp;.

  7. Write a program that prompts the user for an input text file. Open the file and read each word from the file, where a word is defined as any String made up only of upper- and lowercase letters. You can use the next() method in the Scanner class to break up text by whitespace, but your code will still need to examine the input character by character, ending a word when any punctuation or other characters are reached. Store each word (with a count of the number of times you find it) in a binary search tree such as those described in Example 19.10. Then, traverse the tree, printing all the words found (and the number of times found) to the screen in alphabetical order.

  8. Expand the program from Exercise 20.12 so that it also prompts for a second file containing a dictionary in the form of a word list with one word on each line. Store the words from the dictionary in another binary search tree. Then, for each word in the larger document that you can’t find in the dictionary tree, add it to a third binary search tree. Finally, print out the contents of this third binary search tree to the screen, and you will have implemented a rudimentary spell checker. You can test the quality of your implementation by using a novel from Project Gutenberg and a dictionary file from an open-source spell checker or a Scrabble word list.

  9. Files can become corrupted when they’re transferred over a network. It’s common to make a checksum, a short code generated using the entire contents of a file. The checksum can be generated before and after file transmission. If both of the checksums match, there’s a good chance that there were no transmission errors. Of course, there can be problems sending checksums, but checksums are much smaller and therefore less likely to be corrupted. Modern checksums are often generated using cryptographic hash functions, which are more complex than we want to deal with here. An older checksum algorithm works in the following way. Although we use mathematical notation, the operations specified below are integer modulus and integer division.

    1. Add up the values of all the bytes, storing this sum in a long variable

    2. Set sum = sum mod 232

    3. Let r = (sum mod 216) + (sum ÷ 216)

    4. Let s = (r mod 216) + (r ÷ 216)

    5. The final checksum is s

    Remember that finding powers of 2 is easy with bitwise shifts. Write a program that opens a file for binary reading using FileInputStream and outputs the checksum described. On Linux systems, you can check the operation of your program with the sum utility, using the -s option. The following is an example of the command used on a file called wombat.dat. The first number in the output below it, 6892, is the checksum.

sum -s wombat.dat
6892 213 wombat.dat

Experiments

  1. Reading single bytes using either FileInputStream or DataInputStream is slow. It’s much faster to read a block of bytes all at once. Re-implement the RLE bitmap compression program from Section 20.5 using the int read(byte[] b) method from the DataInputStream class, which tries to fill the array b with as many byte values as it can. If there are enough bytes left in the file, the array will be filled completely. If the array is longer than the remaining bytes, only the first part of the array will contain valid bytes. In either case, this method will return the number of byte values successfully read into the array.

    Using a byte array of length 1,024, time the original program against the new version on files with sizes of about 500 KB, 1 MB, and 2 MB. There’s also a void write(byte[] b, int off, int len) method in DataOutputStream that can write an entire array of byte values at once. Using it for output would further increase the speed of your program at the price of greater complexity.

  2. Write the RLE bitmap compression program from Section 20.5 in parallel so that a file is evenly divided into as many pieces as you have threads, compressed, and then each compressed portion is output in the correct order. Compare the speed for 2, 4, and 8 threads to the sequential implementation. Are any of the threaded versions faster? Why or why not? Run some experiments to see how long it takes to read 1,000,000 bytes from a file compared to the time it takes to compress 1,000,000 bytes which are already stored in an array.

21. Network Communication

Arguing with anonymous strangers on the Internet is a sucker’s game because they almost always turn out to be—​or to be indistinguishable from—​self-righteous sixteen-year-olds possessing infinite amounts of free time.

— Neal Stephenson

21.1. Problem: Web server

It’s no accident that the previous chapter about file I/O is followed by this one about networking. At first glance, the two probably seem unrelated. As it happens, both files and networks are used for input and output, and the designers of Java were careful to create an API with a similar interface for both.

In the next two sections, we’ll discuss how this API works, but first we introduce the problem: You need to create a web server application. The term server is used to describe a computer on a network which other computers, called clients, connect to in order to access services or resources. When you browse the Internet, your computer is a client connecting to web servers all over the world. Writing a web server might seem like a daunting task. The web browser you run on your client computer, such as Microsoft Edge, Mozilla Firefox, Apple Safari, or Google Chrome, is a complicated program, capable of streaming audio and video, browsing in multiple tabs, automatically encrypting and decrypting secure information, and at the very least, correctly displaying web pages of every description.

In contrast, a web server application is much simpler. At its heart, a web server gets requests for files and sends those files over the network. More advanced servers can execute code and dynamically generate pages, and many web servers are multi-threaded to support heavy traffic. The web server you’ll write needs only to focus on getting requests for files and sending those files back to the requester.

21.1.1. HTTP requests

To receive requests, a web server uses something called hypertext transfer protocol (HTTP), which is just a way of specifying the format of the requests. The only request we’re interested in is the GET request. All GET requests have the following format.

GET path HTTP/version

In this request, path is the path of the file being requested and version is the HTTP version number. A typical request might be as follows.

GET /images/banner.jpg HTTP/1.1

You should also note that all HTTP commands end with two newline characters ('\n'). The extra blank line makes it easier to separate the commands from other data being sent.

21.1.2. HTTP responses

After your web server receives a GET message, it looks for the file specified by the path. If the server finds the file, it sends the following message.

HTTP/1.1 200 OK

Again, note that this message is followed with two newline characters. After this message is sent, the server sends the requested file, byte by byte, across the network. If the file can’t be found by the web server, it sends an error message as follows.

HTTP/1.1 404 Not Found

Of course, two newlines will be sent after this message as well. After the error message, servers will also typically send some default web page with an explanation in HTML.

Now, we return to the more fundamental problem of how to communicate over a network.

21.2. Concepts: TCP/IP communication

We begin where many discussions of computer networking begin, the Open Systems Interconnection Basic Reference Model (or OSI model). As we mentioned before, the designers of Java wanted to make a networking API which was very similar to the file system API. This single API is intended for JVMs running on Windows or on macOS or on Linux or any other operating system. Even with the same operating system, different computers have different hardware. Some computers have wired connections to a router or gateway. Others are connected wirelessly. Beyond your own computer, you have to figure out the address of the computer you want to send messages to and deal with its network, hardware, and software.

There are so many details in the process that it seems hopelessly complicated. To combat this problem, the OSI seven layer model was developed. Each layer defines a specification for one aspect of the communication path between two computers. As long as a particular layer interacts smoothly with the one above it and below it, that layer could take the form of many different hardware or software choices. Listing them in order from the highest level (closest to the user) to the lowest level (closest to the hardware), the layers are as follows.

  • Layer 7: Application Layer

  • Layer 6: Presentation Layer

  • Layer 5: Session Layer

  • Layer 4: Transport Layer

  • Layer 3: Network Layer

  • Layer 2: Data Link Layer

  • Layer 1: Physical Layer

The application layer is where your code is. The Java networking API calls that your code uses to send and receive data comprise the application layer for your purposes. The only thing above this layer is the user. Protocols like HTTP and FTP are the province of this layer. All the other communication problems have been solved, and the key issue is what to do with the data that’s communicated.

The presentation layer changes one kind of message encoding to another. This layer is not one people usually spend a lot of time worrying about, but some kinds of encryption and compression can happen here.

The session layer allows for the creation of sessions when communicating between computers. Sessions requiring authentication and permissions can be dealt with here, but in practice, this layer’s not often used. One notable exception is Transport Layer Security (TLS), the technology most commonly used to protect passwords and credit card numbers when you make online purchases.

The transport layer is concerned with the making the lower level communication of data more transparent to higher layers. This layer typically breaks larger messages into smaller packets of data which can be sent across the network. This layer can also provide reliability by checking to see if these packets make it to their destinations and resending them otherwise. The two most important protocols for this layer are Transmission Control Protocol (TCP) and User Datagram Protocol (UDP). For Internet traffic, TCP is more commonly used and provides reliable communication that ensures packets are delivered in order. TCP is used for file transfers, e-mail, web browsing, and any number of other web applications. UDP doesn’t have guarantees about reliability or ordering; however, UDP is faster. For this reason, UDP is used for streaming media and online games.

The network layer is responsible for packet transmission from source to destination. It’s concerned with addressing schemes and routing. The most well-known example of a network layer protocol is the Internet Protocol (IP) used to make the Internet work.

If the network layer worries about sending of packets from source to destination, the data link layer is responsible for the actual transmission of packets between each link in the chain. Here hardware becomes more important because there are so many different kinds of networks. Examples of data link layers include Ethernet, token ring networks, IEEE 802.11 Wi-Fi networks, and many more.

Finally, the lowest level is the physical layer. This layer defines the physical specifications for sending raw bits of information from one place to another, over a wire or wirelessly. This layer is typically the least interesting to programmers but is an important area for electrical engineers.

21.3. Syntax: Networking in Java

The seven layer model might seem overwhelming, but there are only a few key pieces we’ll need on a regular basis. In fact, the system of layers is designed to help people focus on the one or two layers specific to their needs and ignore the rest.

21.3.1. Addresses

The first topic we touch on is the network layer. What we need from this layer are addresses. A network address is much like a street address. It gives the location on the network of a computer so that messages can be sent there.

For most systems you use, such an address will be an IP address. There are two current versions of IP addresses, IPv4 and IPv6. IPv6 is the way of the future and provides a huge number of possible addresses. Not all systems support IPv6, and the general public is often not aware of it. Although it will one day be the standard, we use the more common IPv4 addresses here. An IPv4 address is typically written as four decimal numbers separated by dots. Each of these four numbers is in the range 0-255. For example, 64.233.187.99 and 192.168.1.1 are IPv4 addresses.

21.3.2. Sockets

The second topic we focus on is the transport layer. Here, you need to make a choice between TCP or UDP for communication. In this book, we only cover TCP communication because it’s reliable and more commonly used than UDP. If you need to use UDP communication, the basics are not too different from TCP, and there are many excellent resources online.

To create a TCP connection, you typically need a server program and a client program. The difference between the two is not necessarily big. In fact, both the client and the server could be running on the same computer. What distinguishes the server is that it sets up a port and listens on it, waiting for a connection. Once the client makes a connection to that port, the two programs can send and receive data on an equal footing.

We just mentioned the term port. As you know, an address is the location of a computer in a network, but a single computer might be performing many different kinds of network communications. For example, your computer could be running a web browser, an audio chat application, an online game, and a number of other things. So that none of these programs become confused and get each others' messages, each program uses a separate port for communication. To the outside world, your computer usually only has a single address but thousands of available ports. Many of these ports are set aside for specific purposes. For example, port 20 is for FTP, port 23 is for Telnet, and port 80 is for HTTP (web pages).

When you write a server program, you’ll usually create a ServerSocket object linked to a particular port. For example, if you wanted to write a web server, you might create a ServerSocket as follows.

ServerSocket serverSocket = new ServerSocket(80);

Once the ServerSocket object has been created, the server will typically listen to the socket and try to accept incoming connections. When a connection is accepted, a new Socket object is created for that connection. The purpose of the ServerSocket is purely to set up this Socket. The ServerSocket doesn’t do any real communication on its own. This system might seem indirect, but it allows for greater flexibility. For example, a server could have a thread whose only job is to listen for connections. When a connection is made, it could spawn a new thread to do the communication. Commercial web servers often function in this way. The code for a server to listen for a connection is as follows.

Socket socket = serverSocket.accept();

The accept() method is a blocking method; thus, the server will wait for a connection before doing anything else.

Now, if you want to write the client which connects to such a server, you can create the Socket object directly.

Socket socket = new Socket("64.233.187.99", 80);

The first parameter is a String specifying the address of the server, either as an IP address as shown or as a domain like "google.com". The second parameter is the port you want to connect on.

Note that the Socket and ServerSocket classes are both in the java.net package, so you’ll need to import java.net.* to use them and the rest of the basic networking library.

21.3.3. Receiving and sending data

From here on out, we no longer have to worry about the differences between the client and server. Both programs have a Socket object that can be used for communication.

In order to get input from a Socket, you first call its getInputStream() method. You can use the InputStream returned to create an object used for normal file input like in Chapter 20. You will need to make similar considerations about the kind of data you want to read and write. If you only need to receive plain, human-readable text from the Socket, you can create a Scanner object as follows.

Scanner in = new Scanner(socket.getInputStream());

Over the network, it will be more common to send files and other binary data. For that purpose, you can create a DataInputStream or an ObjectInputStream object from the Socket in much the same way.

DataInputStream in = new DataInputStream(socket.getInputStream());

It should be unsurprising that output is just as simple as input. Text output can be accomplished by creating a PrintWriter.

PrintWriter out = new PrintWriter(socket.getOutputStream());

Likewise, binary output can be accomplished by creating an ObjectOutputStream or a DataOutputStream.

DataOutputStream out = new DataOutputStream(socket.getOutputStream());

Once you have these input and output objects, you use them in the same way you would for file processing. There are a few minor differences to keep in mind. In the first place, when reading data, you might not know when more is coming. There’s no explicit end of file. Also, it’s sometimes necessary to call a flush() method after doing a write. A socket might wait for a sizable chunk of data to be accumulated before it gets sent across the network. Without a flush(), the data you write might not be sent immediately.

Example 21.1 Simple client and server

Here’s an example of a piece of server code which listens on port 4321, waits for a connection, reads 100 int values in binary form from the socket, and prints their sum.

try{
    ServerSocket serverSocket = new ServerSocket(4321);
    Socket socket = serverSocket.accept();
    DataInputStream in = new DataInputStream(socket.getInputStream());
    int sum = 0;
    for(int i = 0; i < 100; i++)
        sum += in.readInt();
    in.close();
    System.out.println("Sum: " + sum);
}
catch(IOException e) {
	System.out.println("Network error: " + e.getMessage());
}

Now, here’s a companion piece of client code which connects to port 4321 and sends 100 int values in binary form, specifically the first 100 perfect squares.

try{
    Socket socket = new Socket("127.0.0.1", 4321);
    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
    for(int i = 1; i <= 100; i++)
        out.writeInt(i*i);
    out.close();
}
catch(IOException e) {
	System.out.println("Network error: " + e.getMessage());
}

Note that this client code connects to the IP address 127.0.0.1. This is a special loopback IP address. When you connect to this IP address, it connects to the machine you’re currently working on. In this way, you can test your networking code without needing two separate computers. To test this client and server code together, you will need to run two virtual machines. The simplest way to do so is to open two command line prompts and run the client from one and the server from the other. Be sure that you start the server first so that the client has something to connect to. IDEs like Eclipse and IntelliJ will also allow you to start two programs simultaneously, but it can be difficult to be certain which one is which.

Example 21.2 Chat client and server

Here we look at a more complicated example of network communication, a chat program. If you want to apply the GUI design from Chapter 15, you can make a windowed version of this chat program which looks more like typical chat programs. For now, our chat program is text only.

The functionality of the program is simple. Once connected to a single other chat program, the user will enter his or her name, then enter lines of text each followed by a newline. The program will insert the user’s name at the beginning of each line of text and then send it across the network to the other chat program, which will display it. We encapsulate both client and server functionality in a class called Chat.

import java.io.*;	(1)
import java.net.*;
import java.util.*;

public class Chat {
    private Socket socket;
    
    public static void main(String[] args) {        
        if(args[0].equals("-s")) 		(2)
            new Chat(Integer.parseInt(args[1]));
        else if(args[0].equals("-c"))	(3)
            new Chat(args[1], Integer.parseInt(args[2]) );
        else
            System.out.println("Invalid command line flag.");
    }
1 The first step is the appropriate import statements.
2 In the main() method, if the first command-line argument is "-s", the server version of the Chat constructor will be called. We convert the next argument to an int and use it as a port number.
3 If the argument "-c" is given, the client version of the Chat constructor will be called. We use the next two command-line arguments for the IP address and the port number, respectively.
    // Server
    public Chat(int port) {
        try {
            ServerSocket serverSocket = new ServerSocket(port); (1)
            socket = serverSocket.accept();
            runChat();	(2)
        }
        catch(IOException e) {
			System.out.println("Network error: " + e.getMessage());		
		}         
    }
1 The server Chat constructor takes the port and listens for a connection on it.
2 After a connection, it calls the runChat() method to perform the actual business of sending and receiving chats.
    // Client
    public Chat(String address, int port) { 
		try {	
			socket = new Socket(address, port);
			runChat();
		}
        catch(IOException e) {
			System.out.println("Network error: " + e.getMessage());		
		} 			
    }

The client constructor is similar but connects directly to the specified IP address on the specified port.

    public void runChat() {
        Sender sender = new Sender();		(1)
        Receiver receiver = new Receiver();
        sender.start();						(2)
        receiver.start();
		try {
			sender.join();
			receiver.join();
		}
		catch(InterruptedException e) {}
    }
1 Once the client and server are connected, they both run the runChat() method, which creates a new Sender and a new Receiver to do the sending and receiving.
2 Note that both start() and join() are called on the Sender and Receiver objects. These calls are needed because both classes are subclasses of Thread.

Sending messages is an independent task concerned with reading input from the keyboard and then sending it across the network. Receiving messages is also an independent task, but it’s concerned with reading input from the network and printing it on the screen. Since both tasks are independent, it’s reasonable to allocate a separate thread to each.

Below is the private inner class Sender. In this case it’s convenient but not necessary to make Sender an inner class, especially since it’s so short. The only piece of data Sender shares with Chat is the all important socket variable.

    private class Sender extends Thread {
        public void run() { 
            try {
                PrintWriter netOut = new PrintWriter(socket.getOutputStream()); (1)
                Scanner in = new Scanner(System.in);      
                System.out.print("Enter your name: ");
                String name = in.nextLine();				(2)
                while(!socket.isClosed()) {                  
					String line = in.nextLine(); 			(3)
					if(line.equals("quit"))					(4)
						socket.close();
					else {
						netOut.println(name + ": " + line); (5)
						netOut.flush();
					}                               
                }       
            }
            catch(IOException e) {
				System.out.println("Network error: " + e.getMessage());				
			}         
        }       
    }   
1 The Sender begins by creating a PrintWriter object from the Socket output stream.
2 It reads a name from the user.
3 Then, it waits for a line from the user.
4 If the user types quit, the Socket will be closed.
5 Otherwise, each time a line is read, it’s printed and flushed through the PrintWriter connected to the Socket output stream, with the user name inserted at the beginning.

Below is the private inner class Receiver, the simpler counterpart of Sender.

    private class Receiver extends Thread {
        public void run() {
            try{
                Scanner netIn = new Scanner(socket.getInputStream()); 	(1)
                while(!socket.isClosed())                 
                    if(netIn.hasNextLine())
                        System.out.println(netIn.nextLine());			(2)
            }
            catch(IOException e) {
				System.out.println("Network error: " + e.getMessage());	
			}           
        }
    }
1 First, it creates a Scanner object connected to the input stream of the Socket.
2 Then, waits for a line of text to arrive from the connection. Each time a line arrives, it prints it to the screen.

You can see that this problem is solved with threads much more easily than without them. Both the in.nextLine() method called by Sender and the netIn.nextLine() method called by Receiver are blocking calls. Because each must wait for input before continuing, they can’t easily be combined in one thread of execution.

Although the fundamentals are present in this example, a real chat client should provide a contact list, the opportunity to talk to more than one other user at a time, better error-handling code in the catch-blocks, and many other features. Some of these features are easier to provide in a GUI.

In the next section, we give a solution for the web server problem. Since only the server side is needed, some of the networking is simpler, and there are no threads. However, the communication is done in both binary and text mode.

21.4. Solution: Web server

Here’s our solution to the web server problem. As usual, our solution doesn’t provide all the error checking or features that a real web server would, but it’s entirely functional. When you compile and run the code, it will start a web server on port 80 in the directory you run it from. Feel free to change those settings in the main() method or create a WebServer object from another program. When the server’s running, you should be able to open any web browser and go to http://127.0.0.1. If you put some sample HTML and image files in the directory you run the server from, you should be able to browse them.

import java.io.*;			(1)
import java.net.*;
import java.util.*;

public class WebServer {        
    private int port;		(2)
    private String webRoot;   
    
    public WebServer(int port, String webRoot) {
        this.port = port;
        this.webRoot = webRoot;
    }
    
    public static void main(String[] args) {
		WebServer server = new WebServer(80, new File(".").getAbsolutePath()); (3)
        server.start();
    }
1 Our code starts with the necessary imports.
2 The server has fields for the port where communication will take place and the root directory for the web page.
3 The main() method calls the constructor using port 80 and a path corresponding to the current directory (.) as arguments. Then, it starts the server.

Below is the start() method. This method contains the central loop of the web server that waits for connections and loops forever.

    public void start() {
        Socket socket = null;
        Scanner in = null;
        DataOutputStream out = null;
        String line;
        try {
            ServerSocket serverSocket = new ServerSocket(port); (1)
            while(true) {
                socket = serverSocket.accept(); 
                try {               
                    in = new Scanner(socket.getInputStream()) ;	(2)
                    out = new DataOutputStream(socket.getOutputStream());
					boolean requestRead = false;
                    while(in.hasNextLine() && !requestRead) {	(3)
                        line = in.nextLine();
                        if(line.startsWith("GET")) {
                            String path = line.substring(4,		(4)
								line.lastIndexOf("HTTP")).trim();
                            System.out.println("Received request for: " + path);
                            serve(out, getPath(path));			(5)
                            socket.close();						(6)
                            requestRead = true;
                        }
                    }
                }
                catch(IOException e) {							(7)
                    System.out.println("Error: " + e.getMessage());
                }              
            }
        }
        catch(IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }   
1 First, we create a ServerSocket to listen on the port.
2 Once a connection has been made, the server creates input and output objects from the socket connection. Then, it tries to serve requests coming from the socket input.
3 Our web server keeps reading lines until it finds a GET request.
4 When a GET request is made, the server removes the "GET " at the beginning of the request and the HTTP version information at the end.
5 It passes the remaining file path to the serve() method.
6 Afterward, it closes the connection and sets requestRead to true so that we stop looping through requests made on this particular connection to the server.
7 All that remains in the start() method is the necessary exception handling machinery.

Note that the out object is of type DataOutputStream, allowing us to send binary data over the socket. However, the in variable is of type Scanner, because HTTP requests are text only.

Next is a short utility method which provides some amount of platform independence. The getPath() method takes in a String representation of a path requested by a web browser and formats it. This path should always be given in the Unix or Linux style with slashes (/) separating each directory.

    public String getPath(String path) {
        if('/' != File.separatorChar)				(1)
            path = path.replace('/', File.separatorChar);       
        if(path.endsWith("" + File.separatorChar))
            return webRoot + path + "index.html";	(2)
        else
            return webRoot + path;      
    }
1 To function smoothly with other operating systems, getPath() uses the class variable File.separatorChar which gives the char used to separate directories on whichever platform the JVM is currently running.
2 In addition, getPath() adds "index.html" to the end of the path if the path is a directory rather than a file name. Real web servers try a list of many different files such as index.html, index.htm, index.php, and so on, until a file is found or the list runs out.

The last method in the WebServer class takes in a path and transmits the corresponding file over the network.

    public void serve(DataOutputStream out, String path) throws IOException {
        System.out.println("Trying to serve " + path);
        File file = new File(path);
        if(!file.exists()) {						(1)
            out.writeBytes("HTTP/1.1 404 Not Found\n\n");
            out.writeBytes("<html><head><title>404 Not Found</title></head>" +
				"<body><h1>Not Found</h1>The requested URL " + path +
				" was not found on this server.</body></html>");
            System.out.println("File not found.");
        }
        else {
            out.writeBytes("HTTP/1.1 200 OK\n\n");	(2)
            DataInputStream in = null;
            try {
                in = new DataInputStream(new FileInputStream(file)); (3)
                while(true)
                    out.writeByte(in.readByte());	(4)
            }
			catch(EOFException e) {
				System.out.println("Request succeeded.");
			}
            catch (IOException e) {
                System.out.println("Error sending file: " + e.getMessage());
            }
            finally { try{ in.close(); } catch(Exception e) {} } (5)
        }
    }
}   
1 The serve() method first checks to see if the specified file exists. If it doesn’t, the method sends an HTTP 404 response with a short explanatory piece of HTML. Anyone who’s spent any time on the Internet should be familiar with 404 messages.
2 On the other hand, if the file exists, the method sends an HTTP 200 response indicating success.
3 Then, it creates a new DataInputStream object to read the file. In this case, it’s necessary to read the file in binary. In general, HTML files are human-readable text files, but the image files that web servers must often send such as GIF and JPEG files are binary files filled with unprintable characters. Because we need to send binary data, we were also careful to open a DataOutputStream on the socket earlier.
4 Once the file’s open, the serve() method simply reads it in, byte by byte, and sends each byte out over the socket. It would be more efficient to read a block of bytes and send them together, but our approach is simpler.
5 After the file has been sent, the method closes the DataInputStream and returns.

Because a web server is a real world application, we must repeat the caveat that this implementation is quite bare bones. There are other HTTP requests and many features, including error handling, that a web server should do better. Feel free to extend the functionality.

You might also notice that there’s no way to stop the web server. It has an infinite loop that’s broken only if an IOException is thrown. From a Windows, Linux, or macOS command prompt, you can usually stop a running program by typing Ctrl+C.

21.5. Concurrency: Networking

Throughout this book, we’ve used concurrency primarily for the purpose of speedup. For that kind of performance improvement, concurrency is often icing on the cake. Unless you’re performing massively parallel computations such as code breaking or scientific computing, concurrency will probably make your application run just a little faster or a little smoother.

With network programming, the situation is different. Many networked programs, including chat clients, web servers, and peer-to-peer file sharing software, can be simultaneously connected to tens if not hundreds of other computers at the same time. While there are single-threaded strategies to handle these scenarios, it’s natural to handle them in a multi-threaded way.

A web server at Google, for example, might service thousands of requests per second. If each request had to wait for the previous one to come to completion, the server would become hopelessly bogged down. By using a single thread to listen to requests and then spawn worker threads as needed, the server can run more smoothly.

Even in Example 21.2, it was convenient to create two different threads, Sender and Receiver. We didn’t create them for speedup but simply because they were doing two different jobs. Since the Sender waits for the user to type a line and the Receiver waits for a line of text to arrive over the network, it would be difficult to write a single thread that could handle both jobs. Both threads call the nextLine() method, which blocks execution. A single thread waiting to see if the user had entered more text could not respond to text arriving over the network until the user hit enter.

We only touch briefly on networking in this book. As the Internet evolves, standards and APIs evolve as well. Some libraries can create and manage threads transparently, without the user worrying about the details. In other cases, your program must explicitly use multiple threads to solve the networking problem effectively.

21.6. Exercises

Conceptual Problems

  1. Why are there so many similarities between the network I/O and the file I/O APIs in Java?

  2. Explain the difference between client and server computers in network communication. Is it possible for a single computer to be both a client and a server?

  3. Why is writing a web browser so much more complicated than writing a web server?

  4. Name and briefly describe the seven layers of the OSI model.

  5. Modern computers often have many programs running that are all in communication over a network. Since a computer often has only one IP address that the outside world can send to, how are messages that arrive at the computer connected to the right program?

  6. What are the most popular choices of protocols at the transport layer of the OSI model? What are the advantages and disadvantages of each?

  7. How many possible IP addresses are there in IPv4? IPv6 addresses are often written as eight groups of four hexadecimal digits, totaling 32 hexadecimal digits. How many possible IP addresses are there in IPv6?

Programming Practice

  1. In Example 21.1 a client sends 100 int values, and a server sums them. Rewrite these fragments to send and receive the int values in text rather than binary format. You will need to send whitespace between the values.

  2. Add a GUI based on JFrame for the chat program given in Example 21.2. Use a (non-editable) JTextArea to display the log of messages, including user name. Provide a JTextField for entering messages, a JButton for sending messages, and another JButton for closing the network connections and ending the program.

  3. Study the web server implementation from Section 21.4. Implement a similar web server which is multi-threaded. Instead of serving each request with the same thread that is listening for connections, spawn a new thread to handle the request each time a connection is made.

  4. One of the weaknesses of the web server from the previous exercise is that a new thread has to be created for each connection. An alternative approach is to create a pool of threads to handle requests. Then, when a new request arrives, an idle thread is selected from the pool. Extend the solution to the previous exercise to use a fixed pool of 10 worker threads.

Experiments

  1. The web server program given in Section 21.4 sends files byte by byte. It would be much more efficient to send files in blocks of bytes instead of singly. Re-implement this program to send blocks of 1,024 bytes at a time. Time the difference it takes to send image files with sizes of about 500 KB, 1 MB, and 2 MB using the two different programs. If you can, also measure the time when you’re sending to a different computer, perhaps in the same computer lab. To do so, the person on the other computer will need to enter the IP address of your computer instead of 127.0.0.1. It would also be valuable to time how long it takes to send a file to a computer at a remote location, but doing so often involves changes to firewall settings to allow an outside computer to connect to your server.

  2. Consider the multi-threaded implementation of a web server from Exercise 21.10. Can you design an experiment to measure the average amount of time a client waits to receive the requested file? How does this time change from the single-threaded to the multi-threaded version? If the file size is larger, is the increase in the waiting time the same in both the single- and multi-threaded versions?