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-2s1 s0 = sn-1bn-1 + sn-2bn-2 + … + s1b1 + s0b0

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 s2sn-2 sn-1 = s1b-1 + s2b-2 + … + sn-1b-n+1 + snb-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. ab = ba

    4. a + 0 = a

    5. (ab) ⋅ c = a ⋅ (bc)

    6. a ⋅ (b + c) = (ab) + (ac)

  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 baseheight when the real formula is ½ baseheight. 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 xy, this truth table shows all possible values. As stipulated, xy is true only when both x and y are true.

x y xy

T

T

T

T

F

F

F

T

F

F

F

F

This truth table gives all the values for xy. As you can see, xy is true if x or y are true.

x y xy

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 xy.

x y xy

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 ∨ (yz), 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.

    ¬(xy) = ¬x ∨ ¬y
    ¬(xy) = ¬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 ≤ in, 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: "