Introduction to Java Programming

Cover

A Balanced Textbook with Runnable Examples for Eclipse, NetBeans, IntelliJ IDEA, and Git Bash

Course:Introduction to Java Programming (Object-Oriented Programming in Java)

This book follows the table of contents of the course and is grounded in Introduction to Java Programming (10th edition) by Y. Daniel Liang.Every code listing is a complete, self-contained Java program that compiles and runs under the Java Standard Edition platform used by the course (JDK 22) and works in any IDE—Eclipse, NetBeans, or IntelliJ IDEA—or directly from the Git Bash command line.

Preface

This book teaches Java through runnable examples. Each chapter introduces a coherent set of concepts, illustrates them with two to four short programs, and finishes with a worked example that ties the chapter together. Every program is presented as a complete .java file: the public class name matches the file name, each program has a public static void main(String[] args) entry point, and multi-class examples keep the helper classes in the same file so that a single javac Name.java produces a runnable .class. The code has been compiled and executed with JDK 22; the sample outputs shown beneath each listing are the real outputs captured from those runs.

The book is organized in four parts. Part I introduces Java applications, input/output, and operators. Part II covers additional programming fundamentals: selection, repetition, methods, arrays, and strings. Part III develops object-oriented programming and design—classes, inheritance, polymorphism, interfaces, exceptions, and file I/O. Part IV moves into data structures, collections, lambdas, streams, recursion, searching and sorting, custom generic data structures, and concurrency.

Each chapter ends with a Chapter Summary of key points, a set of Review Questions for self-test, and Programming Exercises that invite you to write your own programs. The examples are deliberately kept small so that you can read them whole; the exercises then ask you to extend them.

How to Use This Book

From Git Bash. Save a program as HelloWorld.java (the file name must match the public class name exactly, including capitalization). Open Git Bash in that folder and run:

javac HelloWorld.java     # compile: produces HelloWorld.class
java HelloWorld          # run: the JVM executes main(String[] args)

If javac is not found, install a JDK and ensure its bin folder is on your PATH (verify with javac -version).

In an IDE (Eclipse, NetBeans, or IntelliJ IDEA). Create a new Java project, add a class named exactly as shown in the listing, paste the code, and click Run. The IDE compiles and runs for you; the command-line workflow above is exactly what every IDE does behind the scenes.

Conventions. Code appears in fenced blocks with a one-line caption naming the file, for example:

Listing: HelloWorld.java

Followed by the code. Sample output then appears in a separate block. Long programs are split across pages only where a blank line naturally occurs. Comments in the code point out the key idea of each section.

A Note on the Examples

Each example is self-contained: it does not depend on custom classes defined elsewhere in the book, and it uses only the Java standard library. Programs that need keyboard input show fixed demo values in a comment so you can run them non-interactively (piping input on the command line) and still see deterministic output. A few examples (card shuffling, random radii, multithreaded interleaving) are deliberately non-deterministic; their sample outputs are labelled "varies."

Compiling Every Example at Once

The code/ folder that accompanies this book contains one subfolder per chapter (code/ch01/code/ch19/) holding the runnable .java files. To compile and run any chapter's examples together from Git Bash:

cd code/ch01
javac *.java       # compiles every .java file in that chapter
java HelloWorld    # run whichever program you want to see

The same command works for every chapter folder, because each example in a chapter uses distinct class names so that javac *.java never reports a duplicate-class conflict.

Part I — Introduction to Java Applications, I/O, and Operators

Chapter 1 — Introduction to Java Applications, Input/Output, and Operators

Java is a general-purpose, object-oriented, platform-independent programming language. This first chapter gets you writing, compiling, and running real Java programs immediately, and it introduces the building blocks you will use in every later chapter: identifiers, variables, data types, operators, type conversions, console input, and output.

After studying this chapter you will be able to:

1.1 What Is Java?

Java was developed by a team led by James Gosling at Sun Microsystems and released as Java 1.0 in 1996. Its original goal was to create a safe, reliable language for smart electronic devices. The designers were dissatisfied that languages like C and C++, while powerful, were prone to memory and security errors—exactly the kinds of errors that could cause critical devices (elevators, microwaves, set-top boxes) to fail. Java's key innovation was automatic memory management (garbage collection), which eliminates whole categories of bugs such as memory leaks and dangling pointers.

From those appliances, Java grew into a general-purpose language. In the late-1990s web revolution, Java applets brought dynamic content to browsers. Today applets are deprecated and removed, but Java itself is everywhere: enterprise servers, cloud systems, Android, ATMs, smart TVs, and embedded devices. It has been maintained by Oracle Corporation since its acquisition of Sun in 2010.

How Java differs from C and C++

Procedural vs. object-oriented programming. Procedural programming designs a program as a set of functions (methods) that manipulate data; it focuses first on how to process data and then on which data structures to use. Object-oriented programming puts data first: it couples data and the methods that operate on that data together into objects, and focuses on the objects and the operations on them. Java is object-oriented at its core.

Java editions. Java comes in several editions:

This book uses Java SE (specifically JDK 22).

1.2 The Java Toolchain: JDK, JRE, and JVM

Three related terms appear constantly in Java; understanding them removes a lot of confusion.

Their relationship is JDK ⊃ JRE ⊃ JVM: the JDK contains the JRE, which contains the JVM.

Bytecode and the compile/run cycle. Unlike C, which compiles to native machine code for one specific platform, Java compiles to bytecode—a platform-independent intermediate format stored in .class files. The flow is:

  1. You write source code in HelloWorld.java.
  2. The compiler javac translates it into bytecode in HelloWorld.class.
  3. The java launcher starts a JVM, loads the .class file, and executes its main method.

Because the .class file is platform-independent, you can compile on one operating system and run the same .class file on another.

1.3 Your First Java Program

Here is the canonical first program. The public class name HelloWorld must match the file name HelloWorld.java exactly—Java is case-sensitive.

Listing: HelloWorld.java

// HelloWorld.java — Your first Java program.
// Demonstrates a class, the main method, console output, and command-line arguments.
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");

        // Show how many command-line arguments were passed (if any).
        System.out.println("Number of command-line arguments: " + args.length);
        for (int i = 0; i < args.length; i++) {
            System.out.println("Argument " + i + ": " + args[i]);
        }
    }
}

Anatomy of the program.

Command-line arguments. When you run java HelloWorld Alice Bob, the args array holds ["Alice", "Bob"] and args.length is 2. The loop in the listing above prints each argument's index and value.

1.4 Compiling and Running in Git Bash and IDEs

From a terminal such as Git Bash, change into the folder that contains HelloWorld.java and type:

javac HelloWorld.java        # compile: produces HelloWorld.class
java HelloWorld Alice Bob    # run: JVM executes main(String[] args)

Sample output:

Hello, World!
Number of command-line arguments: 2
Argument 0: Alice
Argument 1: Bob

In an IDE (Eclipse, NetBeans, or IntelliJ IDEA), the steps are even simpler: create a new Java project, add a class named HelloWorld, paste the code, and click Run. The IDE compiles and runs for you. The command-line workflow is worth knowing regardless, because it is exactly what every IDE does behind the scenes and what you will use in Git Bash.

Common programming errors. A syntax error (also called a compile error) violates the language rules—for example, a missing semicolon or a misspelled Sistem. A runtime error causes the program to terminate abnormally while running—for example, dividing an integer by zero. A logic error compiles and runs but produces the wrong result. The compiler catches syntax errors; testing catches logic errors.

1.5 Identifiers, Variables, and Named Constants

Identifiers are the names of things in a program—variables, constants, methods, classes, and packages. An identifier is a sequence of letters, digits, underscores (_), and dollar signs ($) that

Legal identifiers: $2, area, Area, S_3. Illegal identifiers: 2x (starts with a digit), class (reserved word).

Variables represent values that may change as the program runs. You declare a variable by giving its type and its name:

int count;            // declaration
double radius = 2.5;  // declaration + initialization
int i = 1, j = 2;     // several variables at once

Java has four kinds of variables:

Named constants are identifiers that represent a permanent value. Declare them with final:

final double PI = 3.14159;   // PI cannot be changed afterwards

Using constants has three benefits: you avoid retyping the same literal, you change the value in exactly one place if it ever needs to change, and a descriptive name (MAX_USERS) makes the code easier to read.

Naming conventions (sticking to them makes code readable and avoids errors):

Element Style Example
Class PascalCase HelloWorld
Method / variable camelCase printMessage, totalMarks
Constant UPPER_CASE MAX_SIZE

1.6 Primitive Data Types

Java has eight primitive data types. Six are numeric, plus boolean and char.

Type Range / meaning Storage
byte integers, −128 to 127 8-bit
short integers, −32,768 to 32,767 16-bit
int integers, about ±2.1 billion 32-bit
long integers, about ±9.2 × 10¹⁸ 64-bit
float single-precision floating point, ~7 significant digits 32-bit
double double-precision floating point, ~15 significant digits 64-bit
char a single 16-bit Unicode character 16-bit
boolean true or false 1 bit (logically)

A literal is a constant value written directly in the source. An integer literal that fits is an int; append L for long (2147483648L). A floating-point literal is a double by default; append F for float (100.2F). Integer literals can also be written in binary (0b1010), octal (012), or hexadecimal (0xA). Underscores can group digits for readability: 1_000_000.

double is more accurate than float: 1.0 / 3.0 prints 0.3333333333333333, while 1.0F / 3.0F prints 0.33333334.

1.7 Operators and Expressions

The numeric operators are +, -, *, /, and % (remainder). A key fact for beginners: integer division truncates. 10 / 3 is 3, not 3.333…. To get a fractional result, make at least one operand floating-point, for example (double) a / b.

Operator precedence decides the order of evaluation when operators compete. Multiplication and division bind tighter than addition and subtraction, so a + b * 2 means a + (b * 2). Parentheses override precedence: (a + b) * 2.

Augmented assignment operators combine an operation with assignment: +=, -=, *=, /=, %=. Writing x += 5 is shorthand for x = x + 5.

Increment and decrement. ++ adds one and -- subtracts one. The prefix form (++y) increments first and then uses the new value. The postfix form (y++) uses the current value first and then increments. The next listing demonstrates all of these.

Listing: ArithmeticDemo.java

// ArithmeticDemo.java — Numeric types, operators, precedence, and shortcuts.
public class ArithmeticDemo {
    public static void main(String[] args) {
        int a = 10, b = 3;

        // Basic arithmetic
        System.out.println("a + b = " + (a + b));   // 13
        System.out.println("a - b = " + (a - b));   // 7
        System.out.println("a * b = " + (a * b));   // 30
        System.out.println("a / b = " + (a / b));   // 3  (integer division)
        System.out.println("a % b = " + (a % b));   // 1  (remainder)

        // Integer division vs. floating-point division
        System.out.println("a / b as double = " + ((double) a / b)); // 3.3333...

        // Operator precedence: * and / bind tighter than + and -
        System.out.println("a + b * 2   = " + (a + b * 2));     // 10 + 6 = 16
        System.out.println("(a + b) * 2 = " + ((a + b) * 2));   // 13 * 2 = 26

        // Augmented assignment operators
        int x = 10;
        x += 5;  System.out.println("x += 5 -> " + x);  // 15
        x -= 3;  System.out.println("x -= 3 -> " + x);  // 12
        x *= 2;  System.out.println("x *= 2 -> " + x);  // 24
        x /= 5;  System.out.println("x /= 5 -> " + x);  // 4
        x %= 3;  System.out.println("x %= 3 -> " + x);  // 1

        // Increment and decrement (pre vs. post)
        int y = 5;
        System.out.println("y++  -> " + (y++)); // prints 5, then y becomes 6
        System.out.println("++y  -> " + (++y)); // y becomes 7, prints 7
        System.out.println("y--  -> " + (y--)); // prints 7, then y becomes 6
        System.out.println("--y  -> " + (--y)); // y becomes 5, prints 5
    }
}

Sample output:

a + b = 13
a - b = 7
a * b = 30
a / b = 3
a % b = 1
a / b as double = 3.3333333333333335
a + b * 2   = 16
(a + b) * 2 = 26
x += 5 -> 15
x -= 3 -> 12
x *= 2 -> 24
x /= 5 -> 4
x %= 3 -> 1
y++  -> 5
++y  -> 7
y--  -> 7
--y  -> 5

Comparison operators produce a boolean result: == (equal), != (not equal), <, >, <=, >=. Notice that equality testing uses two equal signs (==); a single = is assignment. We use these heavily from Chapter 2 onward.

1.8 Type Conversions

You can always assign a value to a numeric variable whose type supports a larger range; this is widening and happens automatically:

int i = 100; long l = i; float f = l; double d = f;

Assigning to a type with a smaller range is narrowing and requires an explicit cast, which may lose information:

double price = 9.78;
int dollars = (int) price;   // 9 — the fractional part is truncated

A char can be cast to any numeric type and vice versa. When a char is converted to a number you get its Unicode code point ('A' is 65); when a number is cast to char you get the character at that code point.

Listing: TypeCastingDemo.java

// TypeCastingDemo.java — Widening, narrowing, char<->int, and String concatenation.
public class TypeCastingDemo {
    public static void main(String[] args) {
        // Widening (implicit): smaller range -> larger range
        int i = 100;
        long l = i;          // int -> long
        float f = l;         // long -> float
        double d = f;        // float -> double
        System.out.println("Widening: int " + i + " -> long " + l
                + " -> float " + f + " -> double " + d);

        // Narrowing (explicit cast): larger range -> smaller range
        double price = 9.78;
        int dollars = (int) price; // fractional part is truncated
        System.out.println("Narrowing: double " + price + " -> int " + dollars);

        // char <-> int (Unicode code point)
        char letter = 'A';
        int code = letter;                  // implicit: 'A' -> 65
        System.out.println("char '" + letter + "' has Unicode " + code);
        char nextLetter = (char) (code + 1); // 66 -> 'B'
        System.out.println("Next letter is '" + nextLetter + "'");

        // String concatenation with + (a non-String operand is converted to text)
        String s = "Chapter" + 2;      // "Chapter2"
        String t = "Appendix" + 'B';   // "AppendixB"
        System.out.println(s);
        System.out.println(t);
    }
}

Sample output:

Widening: int 100 -> long 100 -> float 100.0 -> double 100.0
Narrowing: double 9.78 -> int 9
char 'A' has Unicode 65
Next letter is 'B'
Chapter2
AppendixB

1.9 Reading Input with Scanner

The Scanner class (in java.util) reads formatted input from a source such as the keyboard. You create one wrapping System.in and call its methods:

Always import java.util.Scanner; at the top of the file, and close the scanner when you are done (input.close()).

Listing: ScannerDemo.java

// ScannerDemo.java — Reading input from the keyboard with java.util.Scanner.
import java.util.Scanner;

public class ScannerDemo {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        System.out.print("Enter your name: ");
        String name = input.nextLine();

        System.out.print("Enter your age: ");
        int age = input.nextInt();

        System.out.print("Enter your GPA: ");
        double gpa = input.nextDouble();

        System.out.println(); // blank line
        System.out.println("Name : " + name);
        System.out.println("Age  : " + age);
        System.out.println("GPA  : " + gpa);
        System.out.printf("In 10 years you will be %d years old.%n", age + 10);

        input.close();
    }
}

A sample run (user input in bold):

Enter your name: Tasnim
Enter your age: 20
Enter your GPA: 3.85

Name : Tasnim
Age  : 20
GPA  : 3.85
In 10 years you will be 30 years old.

System.out.printf prints formatted output. The format string "In 10 years you will be %d years old.%n" contains a placeholder %d (an integer) that is replaced by the value of age + 10, and %n is a platform-independent newline.

1.10 The String Type

A String is a sequence of characters. String is a predefined class in the Java library—a reference type, not a primitive. A string literal is enclosed in double quotes:

String message = "Welcome to Java";

The + operator is the concatenation operator when at least one operand is a String; any non-String operand is converted to text first. So "Chapter" + 2 yields "Chapter2", and "Appendix" + 'B' yields "AppendixB".

A few useful String methods you will meet often: length() returns the number of characters, charAt(i) returns the character at index i, and toUpperCase() returns an uppercase copy. We treat strings in depth in Chapter 6.

Worked Example: A Sales-Tax Calculator

This program ties the chapter together: it declares a named constant, reads two numbers with Scanner, performs arithmetic, rounds the result with Math.round, and prints a formatted receipt with printf.

Listing: SalesTaxCalculator.java

// SalesTaxCalculator.java — Worked example for Chapter 1.
// Reads a purchase amount and a tax rate from the keyboard, then computes and
// displays the sales tax and the total amount due.
import java.util.Scanner;

public class SalesTaxCalculator {
    public static void main(String[] args) {
        final double CENTS = 100.0; // helper used to round money to the nearest cent

        Scanner input = new Scanner(System.in);

        System.out.print("Enter purchase amount (e.g. 125.50): ");
        double purchase = input.nextDouble();

        System.out.print("Enter tax rate as a percent (e.g. 7.5): ");
        double ratePercent = input.nextDouble();

        double rate = ratePercent / 100.0;   // convert a percent to a fraction
        double tax = purchase * rate;        // compute the sales tax
        double total = purchase + tax;       // compute the total

        // Round each money value to the nearest cent
        tax = Math.round(tax * CENTS) / CENTS;
        total = Math.round(total * CENTS) / CENTS;

        System.out.println(); // blank line
        System.out.printf("Purchase amount: $%8.2f%n", purchase);
        System.out.printf("Tax rate:        %8.2f%%%n", ratePercent);
        System.out.printf("Sales tax:       $%8.2f%n", tax);
        System.out.printf("Total due:       $%8.2f%n", total);

        input.close();
    }
}

A sample run:

Enter purchase amount (e.g. 125.50): 125.50
Enter tax rate as a percent (e.g. 7.5): 7.5

Purchase amount: $  125.50
Tax rate:            7.50%
Sales tax:       $    9.41
Total due:       $  134.91

The format specifier %8.2f means "a floating-point number in a field at least 8 characters wide, with 2 digits after the decimal point." %% prints a literal percent sign.

Chapter Summary

Review Questions

  1. What are the three components denoted by JDK, JRE, and JVM, and how are they related?
  2. What is bytecode, and why is it central to Java's "Write Once, Run Anywhere" promise?
  3. Why must the main method be declared public static void? What would go wrong if static were removed?
  4. Distinguish between a syntax error, a runtime error, and a logic error, giving one example of each.
  5. List the rules a legal Java identifier must obey. Which of 2x, $amount, class, and myVar are legal?
  6. What is the difference between = and ==? Between i = 5 and i == 5?
  7. Explain why 10 / 3 evaluates to 3 in Java. How do you obtain 3.3333…?
  8. What is widening versus narrowing conversion? Give an example of each.
  9. What does the % operator compute for negative operands such as -7 % 2?
  10. Why is String called a reference type rather than a primitive type?

Programming Exercises

  1. Write a program Welcome.java that prints your name, your student ID, and a one-line greeting, each on its own line.
  2. Write a program CircleArea.java that reads a radius from the keyboard and prints the area (π r²) and circumference (2 π r). Use a final double PI = 3.14159; constant.
  3. Write a program SecondsConverter.java that reads a number of seconds and prints it as hours, minutes, and seconds (for example, 7384 seconds → 2 hours, 3 minutes, 4 seconds). Use / and %.
  4. Write a program AverageOfThree.java that reads three double values and prints their average to two decimal places using printf.
  5. Write a program CharInfo.java that reads a single character and prints its Unicode code point, the next character, and the previous character. (Hint: cast char to int.)
  6. Write a program BillSplitter.java that reads a total bill amount and the number of people, then prints how much each person pays, rounded to the nearest cent.

Part II — Additional Programming Fundamentals

Chapter 2 — Control Statements Part I: Selection

A program would be dull if it could only run statements in the order they are written. Selection statements let a program choose among alternative paths of execution based on conditions. This chapter covers Java's selection constructs—the if, if-else, nested if-else-if, and switch statements—together with the logical operators that build compound conditions and the conditional (ternary) operator.

After studying this chapter you will be able to:

2.1 Boolean Expressions and Selection

A Boolean expression is an expression that evaluates to true or false. Selection statements use Boolean expressions as conditions: if the condition is true, one block of statements runs; if false, another runs (or nothing runs). The comparison operators produce Boolean values:

Operator Meaning Example Result
< less than 3 < 5 true
<= less than or equal 5 <= 5 true
> greater than 3 > 5 false
>= greater than or equal 3 >= 5 false
== equal 3 == 3 true
!= not equal 3 != 3 false

Remember that == tests equality, whereas a single = assigns.

2.2 One-way if Statements

A one-way if executes an action only if the condition is true; if the condition is false, nothing happens.

if (boolean-expression) {
    statement(s);
}

The parentheses around the condition are required. The braces can be omitted when the body is a single statement, but keeping them is good practice because it prevents subtle bugs when you later add statements.

2.3 Two-way if-else Statements

A two-way if-else executes one action when the condition is true and another when it is false:

if (boolean-expression) {
    statement(s)-for-the-true-case;
} else {
    statement(s)-for-the-false-case;
}

The next program reads a year and reports whether it is a leap year. It combines comparison operators with the logical operators && (and) and || (or), which we study formally in Section 2.6.

Listing: LeapYear.java

// LeapYear.java — Tests whether a given year is a leap year.
// Uses an if-else with compound boolean expressions (&&, ||).
import java.util.Scanner;

public class LeapYear {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a year: ");
        int year = input.nextInt();

        // A year is a leap year if it is divisible by 4 but not by 100,
        // OR if it is divisible by 400.
        boolean isLeapYear =
            (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);

        if (isLeapYear) {
            System.out.println(year + " is a leap year.");
        } else {
            System.out.println(year + " is not a leap year.");
        }
        input.close();
    }
}

Sample runs:

Enter a year: 2024
2024 is a leap year.

Enter a year: 1900
1900 is not a leap year.

2024 is divisible by 4 and not by 100, so it is a leap year. 1900 is divisible by 100 but not by 400, so it is not a leap year—the || branch (year % 400 == 0) is false and the && branch is false because year % 100 != 0 fails.

2.4 Nested if and Multi-way if-else-if

An if statement can appear inside another if to form a nested if. For mutually exclusive ranges, the multi-way if-else-if ladder is the idiomatic form: conditions are tested top to bottom, and the first one that is true wins; if none is true, the final else runs.

Listing: GradeClassifier.java

// GradeClassifier.java — Multi-way if-else-if to assign a letter grade.
import java.util.Scanner;

public class GradeClassifier {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a score (0-100): ");
        double score = input.nextDouble();

        if (score >= 90.0) {
            System.out.println("Grade: A");
        } else if (score >= 80.0) {
            System.out.println("Grade: B");
        } else if (score >= 70.0) {
            System.out.println("Grade: C");
        } else if (score >= 60.0) {
            System.out.println("Grade: D");
        } else {
            System.out.println("Grade: F");
        }
        input.close();
    }
}

A score of 85 produces Grade: B. Because the conditions are checked in order, each threshold only needs to name its lower bound—the earlier conditions have already ruled out the higher bands.

2.5 Common Errors and Pitfalls

2.6 Logical Operators

Logical operators build compound Boolean expressions.

Operator Name Meaning True when…
! NOT negation the operand is false
&& AND short-circuit conjunction both operands are true
|| OR short-circuit disjunction at least one operand is true
^ XOR exclusive OR exactly one operand is true

Truth table for p and q:

p q !p p && q p || q p ^ q
true true false true true false
true false false false true true
false true true false true true
false false true false false false

Short-circuit evaluation. && and || evaluate the right-hand operand only if needed. In x != 0 && 10 / x > 1, if x is 0 the right side is never evaluated, avoiding division by zero. When you want both sides always evaluated, use the non-short-circuit & and |.

Operator precedence (high to low): parentheses → unary !, ++, --, casts → * / %+ - → comparisons < <= > >= → equality == !=&^|&&|| → assignment. When in doubt, use parentheses—clarity beats cleverness.

2.7 switch Statements

A switch executes statements based on the value of an expression. The switch expression must yield a char, byte, short, int, String, or an enum type; each case label is a constant of a compatible type.

switch (switch-expression) {
    case value1: statement(s); break;
    case value2: statement(s); break;
    ...
    default: statement(s);
}

When a case label matches, execution begins there and falls through to the next case unless a break ends it. Fall-through is sometimes useful: several labels can share one block by stacking them. The default case handles any unmatched value.

Listing: DayOfWeekSwitch.java

// DayOfWeekSwitch.java — A switch statement that maps a number to a day.
import java.util.Scanner;

public class DayOfWeekSwitch {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a day number (1-7): ");
        int day = input.nextInt();

        switch (day) {
            case 1: System.out.println("Monday");    break;
            case 2: System.out.println("Tuesday");   break;
            case 3: System.out.println("Wednesday"); break;
            case 4: System.out.println("Thursday");  break;
            case 5: System.out.println("Friday");    break;
            case 6: System.out.println("Saturday");  break;
            case 7: System.out.println("Sunday");    break;
            default: System.out.println("Invalid day number!");
        }
        input.close();
    }
}

Entering 3 prints Wednesday; entering 9 prints Invalid day number!.

2.8 Conditional Expressions

The conditional operator ? : is a concise two-way choice written inside an expression:

boolean-expression ? expression1 : expression2

It evaluates to expression1 if the condition is true, otherwise to expression2. For example, max = (num1 > num2) ? num1 : num2; assigns the larger of two values. Use it for simple choices; reach for a full if-else when the branches are long.

Listing: ConditionalOperatorDemo.java

// ConditionalOperatorDemo.java — The conditional (ternary) operator ?:
public class ConditionalOperatorDemo {
    public static void main(String[] args) {
        int num1 = 7, num2 = 12;
        int max = (num1 > num2) ? num1 : num2;
        System.out.println("The larger of " + num1 + " and " + num2 + " is " + max);

        int score = 85;
        String status = (score >= 60) ? "Pass" : "Fail";
        System.out.println("Status: " + status);
    }
}

Output:

The larger of 7 and 12 is 12
Status: Pass

Worked Example: How Many Days in a Month?

This program combines a switch (using fall-through to group months that share a day-count) with a leap-year test for February. It reads a month and a year and reports the number of days.

Listing: DaysInMonth.java

// DaysInMonth.java — Worked example for Chapter 2.
// Reads a month (1-12) and a year, then prints how many days are in that month.
// Combines a switch statement (fall-through) with an if-style leap-year test for February.
import java.util.Scanner;

public class DaysInMonth {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a month (1-12): ");
        int month = input.nextInt();
        System.out.print("Enter a year (e.g. 2024): ");
        int year = input.nextInt();

        int days;
        switch (month) {
            case 1: case 3: case 5: case 7: case 8: case 10: case 12:
                days = 31;
                break;
            case 4: case 6: case 9: case 11:
                days = 30;
                break;
            case 2:
                boolean isLeapYear =
                    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
                days = isLeapYear ? 29 : 28;
                break;
            default:
                days = 0;
                System.out.println("Invalid month!");
        }

        if (days != 0) {
            System.out.println("Month " + month + " of " + year
                + " has " + days + " days.");
        }
        input.close();
    }
}

Sample runs:

Enter a month (1-12): 2
Enter a year (e.g. 2024): 2024
Month 2 of 2024 has 29 days.

Enter a month (1-12): 2
Enter a year (e.g. 2024): 1900
Month 2 of 1900 has 28 days.

The stacked case labels (e.g. case 1: case 3: …) deliberately fall through to a single days = 31;, demonstrating the useful side of fall-through. February's case computes the leap-year flag with the same compound condition from LeapYear.java and then uses the conditional operator to pick 29 or 28.

Chapter Summary

Review Questions

  1. What is a Boolean expression, and what values can it produce?
  2. Rewrite if (x = true) so it correctly tests whether x is true. Why is the original problematic?
  3. Trace the multi-way ladder in GradeClassifier.java for a score of 72. Which branch runs, and why are no upper bounds needed?
  4. What is the dangling-else problem, and how do braces resolve it?
  5. For p = true and q = false, evaluate !p, p && q, p || q, and p ^ q.
  6. What does short-circuit evaluation mean, and how can it prevent a division-by-zero error?
  7. What types are allowed for a switch expression in modern Java?
  8. What happens in a switch if you omit a break? When is that behavior desirable?
  9. Write the conditional expression that returns "even" or "odd" for an int n.
  10. Give two situations where a switch is clearer than an if-else-if ladder, and one where it is not applicable.

Programming Exercises

  1. Write a program OddOrEven.java that reads an integer and prints whether it is odd or even.
  2. Write a program LargestOfThree.java that reads three integers and prints the largest. Use nested if statements.
  3. Write a program Season.java that reads a month number (1–12) and prints the season (e.g. 12,1,2 → Winter).
  4. Write a program SimpleCalculator.java that reads two numbers and an operator (+ - * /) and prints the result, using a switch. Handle division by zero.
  5. Write a program Quadrant.java that reads a point (x, y) and prints which quadrant of the Cartesian plane it lies in (or the axis it lies on).
  6. Write a program IncomeTax.java that reads a taxable income and computes the tax using three brackets (e.g. 0–10k at 10%, 10k–50k at 15%, above 50k at 20%) using an if-else-if ladder.

Chapter 3 — Control Statements Part II: Repetition

Loops tell a program to execute statements repeatedly. Java provides three loop constructs—while, do-while, and for—plus the break and continue keywords for finer control. Together with the selection statements of Chapter 2, loops let you express any algorithm. (The logical operators &&, ||, !, and ^ that appear in loop conditions were introduced in Section 2.6.)

After studying this chapter you will be able to:

3.1 Counter-Controlled vs. Sentinel-Controlled Loops

Loops come in two flavors:

All three loop constructs can express either style; the choice of construct is mostly about when the condition is tested and how compactly you can write a counter-controlled loop.

3.2 The while Loop

A while loop repeats its body while the condition is true. The condition is tested before each iteration, so the body may run zero times.

while (loop-continuation-condition) {
    statement(s);
}

Listing: WhileDemo.java

// WhileDemo.java — Counter-controlled while loop: sum the integers 1..100.
public class WhileDemo {
    public static void main(String[] args) {
        int sum = 0;
        int i = 1;
        while (i <= 100) {
            sum += i;
            i++;
        }
        System.out.println("Sum of 1..100 = " + sum); // 5050
    }
}

Output:

Sum of 1..100 = 5050

Three things every counter-controlled while loop needs: initialization (int i = 1), a continuation condition (i <= 100), and an update that moves toward termination (i++). Forgetting the update produces an infinite loop.

3.3 The do-while Loop

A do-while loop is like a while loop except that it executes the body first and tests the condition afterwards, so the body always runs at least once.

do {
    statement(s);
} while (loop-continuation-condition);

Note the trailing semicolon after the condition—it is required.

Listing: DoWhileDemo.java

// DoWhileDemo.java — do-while loop: keep halving a number while it is >= 1.
// The body always runs at least once before the condition is checked.
public class DoWhileDemo {
    public static void main(String[] args) {
        double value = 100.0;
        int steps = 0;
        do {
            System.out.printf("step %d: %.4f%n", steps, value);
            value = value / 2.0;
            steps++;
        } while (value >= 1.0);
        System.out.println("Stopped after " + steps + " halvings.");
    }
}

Output:

step 0: 100.0000
step 1: 50.0000
step 2: 25.0000
step 3: 12.5000
step 4: 6.2500
step 5: 3.1250
step 6: 1.5625
Stopped after 7 halvings.

Use do-while when the body must execute at least once—typical for menus or "read, then test" input patterns.

3.4 The for Loop

The for loop gathers the three parts of a counter-controlled loop into one concise header:

for (initial-action; loop-continuation-condition; action-after-each-iteration) {
    statement(s);
}

The initial action runs once; the condition is tested before each iteration; the update runs after each iteration. Any of the three parts may be omitted, and the initial action and update may be comma-separated lists.

Listing: ForLoopDemo.java

// ForLoopDemo.java — Common for-loop patterns.
public class ForLoopDemo {
    public static void main(String[] args) {
        // Sum 1..100
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println("Sum of 1..100 = " + sum); // 5050

        // Sum of even numbers from 1..100
        int evenSum = 0;
        for (int i = 2; i <= 100; i += 2) {
            evenSum += i;
        }
        System.out.println("Sum of evens 1..100 = " + evenSum); // 2550

        // Count down
        for (int i = 5; i >= 1; i--) {
            System.out.print(i + " ");
        }
        System.out.println();
    }
}

Output:

Sum of 1..100 = 5050
Sum of evens 1..100 = 2550
5 4 3 2 1

A variable declared in the for header (like int i) is scoped to the loop—it cannot be used after the loop ends.

3.5 Nested Loops

A nested loop is a loop inside another loop. Each time the outer loop repeats, the inner loop is entered afresh and runs to completion. Nested loops are the natural way to process tables, grids, and combinations.

Listing: MultiplicationTable.java

// MultiplicationTable.java — Nested for loops print a 10x10 multiplication table.
public class MultiplicationTable {
    public static void main(String[] args) {
        final int SIZE = 10;

        // Column header
        System.out.print("    ");
        for (int j = 1; j <= SIZE; j++) {
            System.out.printf("%4d", j);
        }
        System.out.println();
        System.out.println("    " + "----".repeat(SIZE));

        // Table body: the outer loop drives rows, the inner loop drives columns
        for (int i = 1; i <= SIZE; i++) {
            System.out.printf("%3d|", i);
            for (int j = 1; j <= SIZE; j++) {
                System.out.printf("%4d", i * j);
            }
            System.out.println();
        }
    }
}

Output:

       1   2   3   4   5   6   7   8   9  10
    ----------------------------------------
  1|   1   2   3   4   5   6   7   8   9  10
  2|   2   4   6   8  10  12  14  16  18  20
  3|   3   6   9  12  15  18  21  24  27  30
  4|   4   8  12  16  20  24  28  32  36  40
  5|   5  10  15  20  25  30  35  40  45  50
  6|   6  12  18  24  30  36  42  48  54  60
  7|   7  14  21  28  35  42  49  56  63  70
  8|   8  16  24  32  40  48  56  64  72  80
  9|   9  18  27  36  45  54  63  72  81  90
 10|  10  20  30  40  50  60  70  80  90 100

For a table of size n, the outer and inner loops each run n times, so the body runs n × n times.

3.6 break and continue

The break keyword immediately exits the enclosing loop. The continue keyword ends the current iteration and jumps to the next one. Both also work inside switch (break only).

Listing: BreakContinueDemo.java

// BreakContinueDemo.java — break exits a loop; continue skips to the next iteration.
public class BreakContinueDemo {
    public static void main(String[] args) {
        // break: stop the loop entirely when i reaches 5
        System.out.print("break demo: ");
        for (int i = 1; i <= 10; i++) {
            if (i == 5) break;
            System.out.print(i + " ");
        }
        System.out.println();

        // continue: skip only the number 5
        System.out.print("continue demo: ");
        for (int i = 1; i <= 10; i++) {
            if (i == 5) continue;
            System.out.print(i + " ");
        }
        System.out.println();

        // Sum 0..19 but stop early once the running sum reaches/exceeds 100
        int sum = 0;
        int n;
        for (n = 0; n < 20; n++) {
            sum += n;
            if (sum >= 100) break;
        }
        System.out.println("Broke at n = " + n + ", sum = " + sum);
    }
}

Output:

break demo: 1 2 3 4
continue demo: 1 2 3 4 6 7 8 9 10
Broke at n = 14, sum = 105

Use break and continue sparingly: they can make control flow harder to follow. A well-chosen loop condition is usually clearer.

3.7 Infinite Loops and Common Errors

An infinite loop never terminates because its continuation condition never becomes false. The classic cause is forgetting to update the control variable:

int i = 1;
while (i <= 100) {     // i never changes -> infinite loop
    sum += i;
}

Other common errors:

If you accidentally start an infinite loop in Git Bash, stop it with Ctrl + C.

Worked Example: Listing Prime Numbers

A number is prime if it is greater than 1 and divisible only by 1 and itself. This program prints every prime from 2 up to a limit. It uses an outer for over candidate numbers and an inner for over trial divisors; the inner loop breaks as soon as a divisor is found, because one divisor is enough to prove the number is composite. The trial divisors only need to go up to √n, expressed as d * d <= number to avoid a floating-point Math.sqrt.

Listing: PrimeLister.java

// PrimeLister.java — Worked example for Chapter 3.
// Prints every prime number from 2 up to a limit, using nested for loops and break.
public class PrimeLister {
    public static void main(String[] args) {
        int limit = 50;
        System.out.println("Primes up to " + limit + ":");

        for (int number = 2; number <= limit; number++) {
            boolean isPrime = true;
            // Only need to test divisors up to sqrt(number): d*d <= number.
            for (int d = 2; d * d <= number; d++) {
                if (number % d == 0) {
                    isPrime = false;
                    break; // one divisor is enough to prove it is composite
                }
            }
            if (isPrime) {
                System.out.print(number + " ");
            }
        }
        System.out.println();
    }
}

Output:

Primes up to 50:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47

The d * d <= number test is an optimization: if number has a divisor, it has one no larger than its square root. Without the break, the inner loop would keep testing divisors after already proving compositeness—wasteful but still correct.

Chapter Summary

Review Questions

  1. When should you choose a do-while loop over a while loop?
  2. Rewrite WhileDemo.java as a for loop. Which form is more concise here, and why?
  3. What is the difference between counter-controlled and sentinel-controlled loops? Give an example of each.
  4. Trace the nested loops in MultiplicationTable.java: how many times does the inner System.out.printf execute?
  5. What does continue do, and how does its effect differ from break?
  6. Why is for (double x = 0; x != 1; x += 0.1) a dangerous loop? How would you fix it?
  7. What happens if you write a semicolon immediately after a for header?
  8. In PrimeLister.java, why is the inner-loop condition d * d <= number instead of d <= number?
  9. How do you stop an infinite loop run from Git Bash?
  10. Give one situation where break improves clarity and one where a better loop condition would be clearer than break.

Programming Exercises

  1. Write a program Factorial.java that reads a non-negative integer n and prints n! using a for loop.
  2. Write a program SumDigits.java that reads an integer and prints the sum of its digits using a while loop and % / /.
  3. Write a program Fibonacci.java that prints the first 20 Fibonacci numbers.
  4. Write a program GCD.java that reads two integers and computes their greatest common divisor using Euclid's algorithm in a while loop.
  5. Write a program AverageSentinel.java that reads doubles until the user enters 0, then prints their average. Use 0 as the sentinel.
  6. Write a program Pyramid.java that reads an integer n and prints a pyramid of n rows of asterisks using nested loops.

Chapter 4 — Methods: A Deeper Look

A method is a collection of statements grouped together to perform an operation. Methods let you write a piece of logic once and reuse it, which makes code clearer, shorter, easier to maintain, and easier to debug. This chapter shows how to define and call methods, how arguments are passed, how to overload methods, and how variable scope works.

After studying this chapter you will be able to:

4.1 Defining a Method

A method definition consists of a modifier, a return value type, a method name, a parameter list, and a body:

modifier returnValueType methodName(list of parameters) {
    // method body
}

The return value type is the data type of the value the method returns. A method that performs an action but returns no value uses the keyword void as the return type—such a method is called a void method; otherwise it is a value-returning method. A value-returning method must reach a return statement that yields a value of the declared type.

The classic motivation for methods is reusable code. Instead of writing the same summation loop three times for different ranges, write it once:

Listing: SumCalculator.java

// SumCalculator.java — A reusable method replaces repeated loop code.
public class SumCalculator {
    public static void main(String[] args) {
        System.out.println("Sum from 1 to 10 is " + sum(1, 10));
        System.out.println("Sum from 20 to 37 is " + sum(20, 37));
        System.out.println("Sum from 35 to 49 is " + sum(35, 49));
    }

    /** Return the sum of the integers from i1 to i2 inclusive. */
    public static int sum(int i1, int i2) {
        int result = 0;
        for (int i = i1; i <= i2; i++) {
            result += i;
        }
        return result;
    }
}

Output:

Sum from 1 to 10 is 55
Sum from 20 to 37 is 513
Sum from 35 to 49 is 630

The public static modifiers mean the method is accessible from anywhere and can be called without creating an object—essential for methods called from main, which itself is static.

4.2 Calling a Method

Calling a method executes its body. For a value-returning method, the call is usually used as a value: int larger = max(3, 4);. For a void method, the call is a statement: printGrade(78.5);.

When a method is invoked, the system creates an activation record (also called a stack frame) that stores the method's parameters and local variables. Activation records live on the call stack. When method A calls method B, A's record stays put and a new record for B is pushed on top; when B returns, its record is popped and control returns to A. A method returns control to its caller either when a return statement executes or when its closing brace is reached (for void methods).

4.3 void Methods and Value-Returning Methods

A void method does an action but produces no value to be used in an expression. The next program reads a score and prints the corresponding letter grade using a void helper.

Listing: VoidMethodDemo.java

// VoidMethodDemo.java — A void method performs an action but returns no value.
import java.util.Scanner;

public class VoidMethodDemo {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a score: ");
        double score = input.nextDouble();
        System.out.print("The grade is ");
        printGrade(score);
        input.close();
    }

    /** Prints the letter grade for the given score (no return value). */
    public static void printGrade(double score) {
        if (score >= 90.0)      System.out.println('A');
        else if (score >= 80.0) System.out.println('B');
        else if (score >= 70.0) System.out.println('C');
        else if (score >= 60.0) System.out.println('D');
        else                    System.out.println('F');
    }
}

A sample run:

Enter a score: 78.5
The grade is C

4.4 Passing Arguments by Value

When you call a method, you supply arguments that must match the parameters in order, number, and compatible type. Java passes arguments by value: the argument's value is copied into the parameter. For a primitive variable, this means changes to the parameter inside the method do not affect the caller's variable.

Listing: PassByValueDemo.java

// PassByValueDemo.java — Primitive arguments are copied; the caller's variable is unaffected.
public class PassByValueDemo {
    public static void main(String[] args) {
        int x = 1;
        System.out.println("Before the call, x is " + x);
        increment(x);
        System.out.println("After the call, x is " + x);
    }

    public static void increment(int n) {
        n++;
        System.out.println("n inside the method is " + n);
    }
}

Output:

Before the call, x is 1
n inside the method is 2
After the call, x is 1

Inside the method, n becomes 2, but x in main is still 1—the method received a copy of x's value. (When we pass objects in Chapter 8, we will see that the reference is copied, so the method can change the object's contents through it.)

4.5 Modularizing Code

Modularizing means splitting a program into small, focused methods. Benefits: the code is clearer and easier to read; each computation is isolated, which narrows the scope of debugging; and the methods can be reused in other programs. A good rule of thumb: if you find yourself copying a block of code, extract a method.

4.6 Overloading Methods

Overloading lets you define multiple methods with the same name as long as their signatures (parameter lists) differ in number, type, or order of parameters. The compiler picks the most specific matching method for each call. Overloading is how max can work for two ints, two doubles, or three doubles under one name.

Listing: MethodOverloadingDemo.java

// MethodOverloadingDemo.java — Several methods share a name but differ in parameters.
public class MethodOverloadingDemo {
    public static void main(String[] args) {
        System.out.println("max(3, 4)          = " + max(3, 4));
        System.out.println("max(3.0, 9.5)      = " + max(3.0, 9.5));
        System.out.println("max(3.0, 9.5, 7.1) = " + max(3.0, 9.5, 7.1));
    }

    public static int max(int num1, int num2) {
        return (num1 > num2) ? num1 : num2;
    }

    public static double max(double num1, double num2) {
        return (num1 > num2) ? num1 : num2;
    }

    public static double max(double num1, double num2, double num3) {
        return max(max(num1, num2), num3);
    }
}

Output:

max(3, 4)          = 4
max(3.0, 9.5)      = 9.5
max(3.0, 9.5, 7.1) = 9.5

The call max(3, 4) matches the int version; max(3.0, 9.5) matches the two-double version; max(3.0, 9.5, 7.1) matches the three-double version, which itself calls the two-double version. Return type alone is not enough to distinguish overloads—only the parameter list matters.

4.7 Scope of Variables

The scope of a variable is the part of the program where it can be referenced. A local variable declared inside a method is usable from its declaration to the end of the enclosing block. A variable declared in a for header (for (int i = …)) is scoped to the entire loop. You may reuse the same local-variable name in different, non-nested blocks, but you cannot declare two local variables with the same name in the same block or in nested blocks.

4.8 The Math Class

The Math class (java.lang.Math) provides useful static methods and constants you can call without an object:

The worked example uses Math.pow.

Worked Example: A Mortgage Calculator

This program reads a loan principal, an annual interest rate, and a term in years, then computes the fixed monthly payment using the standard amortization formula. The formula is isolated in its own value-returning method, illustrating modularization, and it uses Math.pow.

Listing: MortgageCalculator.java

// MortgageCalculator.java — Worked example for Chapter 4.
// Computes a monthly mortgage payment using a value-returning method and Math.pow.
import java.util.Scanner;

public class MortgageCalculator {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Loan principal (e.g. 250000): ");
        double principal = input.nextDouble();
        System.out.print("Annual interest rate percent (e.g. 6.5): ");
        double annualRate = input.nextDouble();
        System.out.print("Loan term in years (e.g. 30): ");
        int years = input.nextInt();

        double monthly = monthlyPayment(principal, annualRate, years);
        System.out.printf("Monthly payment: $%.2f%n", monthly);
        System.out.printf("Total paid over %d years: $%.2f%n",
            years, monthly * 12 * years);
        input.close();
    }

    /**
     * Monthly payment = P * r * (1+r)^n / ((1+r)^n - 1),
     * where r is the monthly rate (as a fraction) and n is the number of months.
     */
    public static double monthlyPayment(double principal,
                                        double annualRatePercent, int years) {
        int months = years * 12;
        double r = annualRatePercent / 100.0 / 12.0; // monthly rate as a fraction
        if (r == 0) {
            return principal / months; // zero-interest loan
        }
        double factor = Math.pow(1 + r, months);
        return principal * r * factor / (factor - 1);
    }
}

A sample run:

Loan principal (e.g. 250000): 250000
Annual interest rate percent (e.g. 6.5): 6.5
Loan term in years (e.g. 30): 30
Monthly payment: $1580.17
Total paid over 30 years: $568861.22

A $250,000 loan at 6.5% for 30 years costs about $1,580.17 per month and about $568,861.22 over the life of the loan—more than twice the principal, because of interest. The zero-interest guard (if (r == 0)) prevents division by zero when factor - 1 would be 0.

Chapter Summary

Review Questions

  1. List the five parts of a method definition.
  2. What is the difference between a void method and a value-returning method? How do you call each?
  3. What is an activation record, and what role does the call stack play when methods call one another?
  4. In PassByValueDemo.java, why does x remain 1 after the call to increment?
  5. When is a static method necessary? Why is main declared static?
  6. What three things must match between a method's parameters and the arguments at a call site?
  7. Can two overloaded methods differ only in return type? Why or why not?
  8. What is the scope of a variable declared in a for loop header?
  9. Name four useful methods of the Math class and what each does.
  10. Give one example from this chapter where extracting a method removed duplicated code.

Programming Exercises

  1. Write a program with a method int cube(int n) that returns . Call it from main for several values.
  2. Write a program with an overloaded method area that computes the area of a circle (given radius) and of a rectangle (given width and height).
  3. Write a program with a method boolean isEven(int n) and use it to print whether each number from 1 to 10 is even.
  4. Write a program with a void method printRow(int n) that prints the multiplication table row for n (1×n … 10×n); call it for n = 1..10.
  5. Write a program with a method double average(double a, double b, double c) and use it to average three numbers read from the keyboard.
  6. Write a program with a method int reverse(int n) that returns the digits of n reversed (e.g. 12344321).

Chapter 5 — Arrays and ArrayLists

An array is a data structure that stores a fixed-size, sequential collection of elements of the same type. A single array variable can reference a large collection of data, which lets you process many values with short, uniform code. This chapter covers declaring, creating, initializing, and processing arrays; copying and passing arrays; variable-length argument lists; the resizable ArrayList; and multidimensional arrays.

After studying this chapter you will be able to:

5.1 Declaring and Creating Arrays

To use an array you declare a variable to reference it and specify the element type. The bracket notation elementType[] marks the variable as an array:

double[] myList;          // declaration (no space allocated yet)
myList = new double[10];  // creation: 10 doubles, each defaulting to 0.0

Declaration alone creates only a storage location for the reference; the variable is null until an array is assigned. Creation with new elementType[size] allocates the storage and assigns the reference. The two steps are usually combined:

double[] myList = new double[10];

When an array is created, its elements receive default values: 0 for numeric types, false for boolean, '\u0000' for char, and null for reference types. The size is fixed at creation and cannot change; obtain it with myList.length (note: length is a property, not a method—no parentheses).

5.2 Array Initializers and Processing

An array initializer combines declaration, creation, and initialization in one statement:

double[] values = {1.9, 2.5, 3.4, 4.5};

Array indices are 0-based, ranging from 0 to length - 1. Accessing an index outside that range throws ArrayIndexOutOfBoundsException at runtime—a very common beginner error.

Listing: ArrayBasicsDemo.java

// ArrayBasicsDemo.java — Declaring, creating, initializing, and processing arrays.
public class ArrayBasicsDemo {
    public static void main(String[] args) {
        // Declare and create an array of 5 doubles (default element value is 0.0)
        double[] myList = new double[5];
        for (int i = 0; i < myList.length; i++) {
            myList[i] = i * i; // assign values 0, 1, 4, 9, 16
        }

        // Array initializer shorthand
        double[] values = {1.9, 2.5, 3.4, 4.5};

        // Process with an indexed for loop: sum
        double sum = 0;
        for (int i = 0; i < values.length; i++) {
            sum += values[i];
        }
        System.out.println("Sum of values = " + sum);
        System.out.println("Average = " + (sum / values.length));

        // Foreach loop: print each element
        System.out.print("values: ");
        for (double v : values) {
            System.out.print(v + " ");
        }
        System.out.println();

        // Find the max
        double max = values[0];
        for (double v : values) {
            if (v > max) max = v;
        }
        System.out.println("Max = " + max);
        System.out.println("myList length = " + myList.length);
    }
}

Output:

Sum of values = 12.3
Average = 3.075
values: 1.9 2.5 3.4 4.5
Max = 4.5
myList length = 5

The foreach loop (for (double v : values)) reads "for each element v in values." It is concise and avoids index bugs, but it is read-only: you cannot assign to v to change the array, and you do not have the index.

5.3 Case Study: Analyzing Numbers

A common task is to read a set of numbers, compute their average, and count how many are above average. The array size can come from the user at runtime.

Listing: AnalyzeNumbers.java

// AnalyzeNumbers.java — Reads n numbers, computes the average, and counts how many
// are above the average. Demonstrates creating an array from a runtime size.
import java.util.Scanner;

public class AnalyzeNumbers {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter the number of items: ");
        int n = input.nextInt();

        double[] numbers = new double[n];
        double sum = 0;

        System.out.print("Enter the numbers: ");
        for (int i = 0; i < n; i++) {
            numbers[i] = input.nextDouble();
            sum += numbers[i];
        }

        double average = sum / n;
        int count = 0;
        for (double num : numbers) {
            if (num > average) count++;
        }

        System.out.printf("Average = %.2f%n", average);
        System.out.println("Number of elements above the average = " + count);
        input.close();
    }
}

A sample run (entering 5 then 1 2 3 4 5):

Enter the number of items: 5
Enter the numbers: 1 2 3 4 5
Average = 3.00
Number of elements above the average = 2

The average is 3.00, and exactly two values (4 and 5) are above it.

5.4 Copying Arrays

The assignment operator does not copy an array's contents—it copies the reference, so both names point to the same array. To copy contents, either loop element by element, use System.arraycopy, or use java.util.Arrays.copyOf.

Listing: ArrayCopyAndPassDemo.java

// ArrayCopyAndPassDemo.java — Copying arrays, passing arrays to methods,
// returning arrays, and variable-length argument lists (varargs).
public class ArrayCopyAndPassDemo {
    public static void main(String[] args) {
        // Copying: = copies the reference, not the contents
        int[] a = {1, 2, 3};
        int[] b = a;          // b now refers to the SAME array as a
        b[0] = 99;
        System.out.println("After b[0]=99, a[0] = " + a[0]); // 99 — same array

        // A proper copy uses a loop (or Arrays.copyOf / System.arraycopy)
        int[] c = copy(a);
        c[0] = 0;
        System.out.println("After c[0]=0,  a[0] = " + a[0]); // still 99 — separate array

        // Passing an array to a method: the method can change its contents
        int[] data = {5, 6, 7};
        doubleAll(data);
        System.out.print("data after doubleAll: ");
        for (int v : data) System.out.print(v + " ");
        System.out.println();

        // Varargs: a variable number of int arguments, treated as an array
        System.out.println("max of (3, 9, 2, 7) = " + max(3, 9, 2, 7));
    }

    /** Return a new array that is a copy of list. */
    public static int[] copy(int[] list) {
        int[] result = new int[list.length];
        for (int i = 0; i < list.length; i++) {
            result[i] = list[i];
        }
        return result;
    }

    /** Double every element of the array (modifies the caller's array). */
    public static void doubleAll(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            arr[i] *= 2;
        }
    }

    /** Variable-length argument list: numbers is treated as int[]. */
    public static int max(int... numbers) {
        int best = numbers[0];
        for (int v : numbers) {
            if (v > best) best = v;
        }
        return best;
    }
}

Output:

After b[0]=99, a[0] = 99
After c[0]=0,  a[0] = 99
data after doubleAll: 10 12 14
max of (3, 9, 2, 7) = 9

Two key behaviors: (1) b = a makes b alias a, so changing b[0] changes a[0]; the copy method returns a separate array, so changing c[0] does not affect a. (2) When you pass an array to a method, the reference is passed by value, so the method can modify the array's contents (as doubleAll does) even though it cannot reassign the caller's variable.

Varargs. The parameter int... numbers lets callers pass any number of int arguments (or an int[]); inside the method, numbers is treated as an array. Only one varargs parameter is allowed per method, and it must be last.

5.5 The ArrayList Class

A regular array has a fixed size. java.util.ArrayList is a resizable array that grows as you add elements. Specify the element type in angle brackets (generics):

Listing: ArrayListDemo.java

// ArrayListDemo.java — Basic use of java.util.ArrayList, a resizable array.
import java.util.ArrayList;

public class ArrayListDemo {
    public static void main(String[] args) {
        ArrayList<String> cities = new ArrayList<>();

        cities.add("Dhaka");
        cities.add("Chittagong");
        cities.add("Sylhet");
        cities.add("Khulna");

        System.out.println("Size: " + cities.size());
        System.out.println("First: " + cities.get(0));
        System.out.println("Index of Sylhet: " + cities.indexOf("Sylhet"));

        cities.remove("Chittagong");
        System.out.println("After removing Chittagong: " + cities);

        // Iterate with a foreach loop
        System.out.print("All cities: ");
        for (String c : cities) {
            System.out.print(c + " ");
        }
        System.out.println();

        System.out.println("Contains Dhaka? " + cities.contains("Dhaka"));
        cities.set(0, "Dhaka City");
        System.out.println("After set(0): " + cities);
    }
}

Output:

Size: 4
First: Dhaka
Index of Sylhet: 2
After removing Chittagong: [Dhaka, Sylhet, Khulna]
All cities: Dhaka Sylhet Khulna
Contains Dhaka? true
After set(0): [Dhaka City, Sylhet, Khulna]

Common ArrayList methods: add(x), get(i), set(i, x), remove(i) or remove(Object), size(), indexOf(x), contains(x), and isEmpty(). We explore the full collections framework in Chapter 14.

5.6 Multidimensional Arrays

A two-dimensional array is an array of arrays, declared with two sets of brackets. Each row is itself a one-dimensional array, so rows can even have different lengths (a ragged array).

Listing: MultidimensionalArrayDemo.java

// MultidimensionalArrayDemo.java — Two-dimensional arrays: declare, fill, print, sum.
public class MultidimensionalArrayDemo {
    public static void main(String[] args) {
        // Declare and create a 3x4 matrix
        int[][] matrix = new int[3][4];

        // Fill it with 1..12
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                matrix[i][j] = i * matrix[i].length + j + 1;
            }
        }

        // Print it row by row
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                System.out.printf("%4d", matrix[i][j]);
            }
            System.out.println();
        }

        // Sum all elements using nested foreach loops
        int total = 0;
        for (int[] row : matrix) {
            for (int v : row) {
                total += v;
            }
        }
        System.out.println("Total = " + total); // 78
    }
}

Output:

   1   2   3   4
   5   6   7   8
   9  10  11  12
Total = 78

matrix.length is the number of rows (3); matrix[i].length is the number of columns in row i (4). The nested foreach reads "for each row (an int[]) in matrix, for each v in row."

Worked Example: Deck of Cards

This program represents a 52-card deck as an int[] of numbers 051, shuffles it by random swaps, and prints four cards. Each number maps to a suit (cardNumber / 13) and a rank (cardNumber % 13) using two String[] lookup tables. It brings together array creation, initialization, processing, and Math.random.

Listing: DeckOfCards.java

// DeckOfCards.java — Worked example for Chapter 5.
// Picks four cards at random from a shuffled 52-card deck using an int[] array.
public class DeckOfCards {
    public static void main(String[] args) {
        int[] deck = new int[52];
        String[] suits = {"Spades", "Hearts", "Diamonds", "Clubs"};
        String[] ranks = {"Ace", "2", "3", "4", "5", "6", "7", "8", "9",
                          "10", "Jack", "Queen", "King"};

        // Initialize the deck: deck[i] = i
        for (int i = 0; i < deck.length; i++) {
            deck[i] = i;
        }

        // Shuffle by swapping each card with a randomly chosen one
        for (int i = 0; i < deck.length; i++) {
            int j = (int) (Math.random() * deck.length);
            int temp = deck[i];
            deck[i] = deck[j];
            deck[j] = temp;
        }

        // Pick the first four cards and display them
        for (int i = 0; i < 4; i++) {
            int cardNumber = deck[i];
            String suit = suits[cardNumber / 13];
            String rank = ranks[cardNumber % 13];
            System.out.println("Card " + (i + 1) + ": " + rank + " of " + suit);
        }
    }
}

One sample run (output changes each run because of shuffling):

Card 1: 2 of Clubs
Card 2: 7 of Hearts
Card 3: 6 of Spades
Card 4: Jack of Hearts

The division and remainder (/ 13 and % 13) are the trick that maps a single number to a (suit, rank) pair—card numbers 012 are Spades, 1325 Hearts, 2638 Diamonds, and 3951 Clubs.

Chapter Summary

Review Questions

  1. What is the difference between declaring an array and creating one? What is the value of an array variable after declaration but before creation?
  2. What are the default element values for an array of int, boolean, double, and String?
  3. Why does myList.length not use parentheses?
  4. What exception is thrown when you access index 5 of a 5-element array, and why?
  5. Explain why list2 = list1 does not give you an independent copy of list1.
  6. Name three ways to copy the contents of an array.
  7. How does passing an array to a method differ from passing a primitive? Can the method change the caller's array contents?
  8. What is a varargs parameter, and what are the rules for declaring one?
  9. List four ArrayList methods and what each does. Why is ArrayList preferable when the size is unknown?
  10. For a 2-D int[][] matrix, what do matrix.length and matrix[0].length represent?

Programming Exercises

  1. Write a program ReverseArray.java that reads n integers into an array and prints them in reverse order.
  2. Write a program MinMaxArray.java that reads n doubles and prints the smallest, the largest, and the average.
  3. Write a program CountOccurrences.java that reads a list of integers and a target value, and prints how many times the target appears.
  4. Write a program ShiftArray.java that shifts every element of an array one position to the left (the first element moves to the end).
  5. Write a program MatrixSum.java that reads a 2×3 and a 3×2 matrix and prints their product.
  6. Write a program DynamicList.java that reads words from the user into an ArrayList<String> until the user types "quit", then prints the list and its size.

Chapter 6 — Strings, Characters, and Regular Expressions

A string is a sequence of characters. Java's String class is a predefined reference type with more than 40 methods for examining and manipulating strings. This chapter covers constructing strings, their immutability, the most useful methods, comparison subtleties, the mutable StringBuilder/StringBuffer classes, and regular expressions for matching, replacing, and splitting.

After studying this chapter you will be able to:

6.1 The String Class

You can create a string from a literal or from an array of characters:

String message = "Welcome to Java";          // from a literal
char[] chars = {'J', 'a', 'v', 'a'};
String s = new String(chars);                 // from a char array

String is a reference type, not a primitive: message is a reference variable pointing to a String object.

Immutable strings. A String object is immutable—once created, its contents cannot change. When you write

String s = "Java";
s = "HTML";

s now refers to a new String object "HTML"; the old "Java" object is unchanged. Methods like toUpperCase() and concat() likewise return new strings rather than modifying the receiver.

Interned strings. To save memory, the JVM uses a single shared instance—a string literal pool—for literals with the same character sequence. So two literals "Java" refer to the same object. A string created with new String("Java") is a distinct object with the same contents. This is the root of the == versus equals issue in Section 6.3.

6.2 Common String Methods

Listing: StringMethodsDemo.java

// StringMethodsDemo.java — Common String methods. Strings are immutable:
// methods return new String objects rather than changing the original.
public class StringMethodsDemo {
    public static void main(String[] args) {
        String s = "Welcome to Java";

        System.out.println("s                = " + s);
        System.out.println("length()         = " + s.length());
        System.out.println("charAt(0)        = " + s.charAt(0));
        System.out.println("concat(\"!\")      = " + s.concat("!"));
        System.out.println("toUpperCase()    = " + s.toUpperCase());
        System.out.println("toLowerCase()    = " + s.toLowerCase());
        System.out.println("\"  hi  \".trim() = " + "  hi  ".trim());
        System.out.println("substring(0,7)   = " + s.substring(0, 7));
        System.out.println("indexOf('a')     = " + s.indexOf('a'));
        System.out.println("lastIndexOf('a') = " + s.lastIndexOf('a'));
        System.out.println("replace          = " + s.replace("Java", "HTML"));
        System.out.println("format           = " + String.format("Pi is %.2f", 3.14159));
    }
}

Output:

s                = Welcome to Java
length()         = 15
charAt(0)        = W
concat("!")      = Welcome to Java!
toUpperCase()    = WELCOME TO JAVA
toLowerCase()    = welcome to java
"  hi  ".trim() = hi
substring(0,7)   = Welcome
indexOf('a')     = 12
lastIndexOf('a') = 14
replace          = Welcome to HTML
format           = Pi is 3.14

Highlights: length() (a method—unlike an array's length property); charAt(i) returns the char at index i (0-based, bounds-checked); substring(begin, end) returns the slice [begin, end); indexOf/lastIndexOf find a character or substring and return -1 if not found; replace substitutes literal text; String.format builds a formatted string (same specifiers as printf).

6.3 Comparing Strings

The == operator checks whether two references point to the same object; it does not compare contents. To compare contents, use equals. The compareTo method gives ordering: it returns 0 if equal, a negative value if the receiver is lexicographically less than the argument, and a positive value if greater.

Listing: StringCompareDemo.java

// StringCompareDemo.java — equals vs ==, interned strings, compareTo.
public class StringCompareDemo {
    public static void main(String[] args) {
        String s1 = "Java";
        String s2 = new String("Java"); // a distinct object with the same contents
        String s3 = "Java";             // interned: same instance as s1

        System.out.println("s1 == s2        : " + (s1 == s2));       // false (different objects)
        System.out.println("s1 == s3        : " + (s1 == s3));       // true  (interned)
        System.out.println("s1.equals(s2)   : " + s1.equals(s2));   // true  (same contents)

        // compareTo: 0 if equal, <0 if s1 < s2 lexicographically, >0 if greater
        System.out.println("\"apple\".compareTo(\"banana\")  : " + "apple".compareTo("banana")); // negative
        System.out.println("\"banana\".compareTo(\"apple\")  : " + "banana".compareTo("apple")); // positive
        System.out.println("\"Java\".compareTo(\"java\")     : " + "Java".compareTo("java"));    // negative

        // Case-insensitive and prefix/suffix checks
        System.out.println("equalsIgnoreCase : " + "Java".equalsIgnoreCase("java"));
        System.out.println("startsWith(\"Wel\"): " + "Welcome".startsWith("Wel"));
        System.out.println("endsWith(\"ome\")  : " + "Welcome".endsWith("ome"));
    }
}

Output:

s1 == s2        : false
s1 == s3        : true
s1.equals(s2)   : true
"apple".compareTo("banana")  : -1
"banana".compareTo("apple")  : 1
"Java".compareTo("java")     : -32
equalsIgnoreCase : true
startsWith("Wel"): true
endsWith("ome")  : true

Rule of thumb: always use equals to compare string contents, and reserve == for checking identity (rarely what you want). compareTo is used when you need ordering (for example, sorting). Because uppercase letters have lower Unicode values than lowercase, "Java".compareTo("java") is negative; use compareToIgnoreCase when case should not matter.

6.4 StringBuilder and StringBuffer

Because String is immutable, repeated concatenation creates many temporary objects. StringBuilder is a mutable character sequence you can append to, insert into, delete from, and reverse in place—far more efficient for building strings in a loop. StringBuffer is the same API but thread-safe (synchronized); prefer StringBuilder when you do not need synchronization.

Listing: StringBuilderDemo.java

// StringBuilderDemo.java — StringBuilder is a mutable sequence of characters.
public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Java");
        sb.append(" is");        // append
        sb.append(" fun");
        sb.insert(0, ">> ");     // insert at index 0
        System.out.println(sb);  // >> Java is fun

        sb.delete(0, 3);         // delete chars at indices 0..2
        System.out.println(sb);  // Java is fun

        sb.reverse();
        System.out.println(sb);  // nuf si avaJ

        sb.reverse();            // back to "Java is fun"
        sb.setCharAt(0, 'j');    // mutate a character in place
        System.out.println(sb);  // java is fun

        System.out.println("Length: " + sb.length());
    }
}

Output:

>> Java is fun
Java is fun
nuf si avaJ
java is fun
Length: 11

Common StringBuilder methods: append(x), insert(i, x), delete(start, end), reverse(), setCharAt(i, ch), charAt(i), length(), and toString() (to get an immutable String back).

6.5 Regular Expressions

A regular expression (regex) is a string that describes a pattern. String provides three regex-aware methods: matches (does the entire string fit the pattern?), replaceAll and replaceFirst (substitute matching parts), and split (break the string into pieces on a delimiter pattern).

Some regex building blocks: . matches any character; * means zero or more of the preceding; + means one or more; [abc] matches any one of a, b, c; [^abc] matches any character except a, b, c; \d matches a digit; \w matches a word character (letter, digit, or underscore).

Listing: RegexDemo.java

// RegexDemo.java — Regular expressions: matches, replaceAll, split.
public class RegexDemo {
    public static void main(String[] args) {
        // matches: does the WHOLE string fit the pattern?
        System.out.println("\"Java is fun\" matches \"Java.*\" : "
            + "Java is fun".matches("Java.*"));
        System.out.println("\"a1b2c\" matches \"[a-z0-9]+\"    : "
            + "a1b2c".matches("[a-z0-9]+"));

        // replaceAll: replace every digit with '*'
        System.out.println("Digits hidden : "
            + "Phone 01712345678".replaceAll("\\d", "*"));

        // replaceAll with a character class: replace $, +, # with '-'
        System.out.println("Symbols gone  : "
            + "a+b$#c".replaceAll("[$+#]", "-"));

        // split: break a string on a delimiter pattern
        String[] tokens = "Java,C?C#,C++".split("[,?]");
        for (String t : tokens) {
            System.out.println("  token: " + t);
        }

        // A simplified email validation pattern
        String email = "user@example.com";
        System.out.println("email valid?  : " + email.matches("\\w+@\\w+\\.\\w+"));
    }
}

Output:

"Java is fun" matches "Java.*" : true
"a1b2c" matches "[a-z0-9]+"    : true
Digits hidden : Phone ***********
Symbols gone  : a-b--c
  token: Java
  token: C
  token: C#
  token: C++
email valid?  : true

In Java string literals a backslash is itself escaped, so a regex digit \d is written "\\d". Note that matches requires the pattern to describe the whole string—"Java".matches("Java") is true, but "Java is fun".matches("Java") is false (use "Java.*" to allow trailing text).

Worked Example: Palindrome Checker

A palindrome reads the same forwards and backwards. This program decides whether a phrase is a palindrome after ignoring case, spaces, and punctuation. It uses replaceAll with the pattern [^a-zA-Z0-9] (any character that is not a letter or digit) to strip noise, then StringBuilder.reverse() to reverse the cleaned text, then equals to compare.

Listing: PalindromeChecker.java

// PalindromeChecker.java — Worked example for Chapter 6.
// Checks whether a phrase is a palindrome, ignoring case, spaces, and punctuation.
// Uses replaceAll (regex), StringBuilder.reverse, and String.equals.
import java.util.Scanner;

public class PalindromeChecker {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a phrase: ");
        String phrase = input.nextLine();
        input.close();

        // Keep only letters and digits, then lowercase
        String cleaned = phrase.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();

        // A palindrome reads the same forwards and backwards
        String reversed = new StringBuilder(cleaned).reverse().toString();
        boolean isPalindrome = cleaned.equals(reversed);

        System.out.println("Cleaned : " + cleaned);
        System.out.println("Reversed: " + reversed);
        System.out.println("Is a palindrome? " + isPalindrome);
    }
}

Sample runs:

Enter a phrase: A man, a plan, a canal: Panama
Cleaned : amanaplanacanalpanama
Reversed: amanaplanacanalpanama
Is a palindrome? true

Enter a phrase: race a car
Cleaned : raceacar
Reversed: racaecar
Is a palindrome? false

The first phrase, stripped to amanaplanacanalpanama, is identical to its reverse. The second, raceacar, is not.

Chapter Summary

Review Questions

  1. What does it mean that String is immutable? What happens to the old object when you reassign a String variable?
  2. Why does s.length() use parentheses while an array's length does not?
  3. Explain the difference between == and equals for strings. Why can == give true for two literals but false for new String("x") and "x"?
  4. What does "apple".compareTo("banana") return, and what does the sign tell you?
  5. Why is uppercase "Java" "less than" lowercase "java" in compareTo?
  6. When should you use StringBuilder instead of String concatenation?
  7. What is the difference between StringBuilder and StringBuffer?
  8. Does "Java is fun".matches("Java") return true? Why or why not?
  9. Why is a digit-matching regex written "\\d" in Java source code?
  10. Describe how PalindromeChecker uses replaceAll, StringBuilder, and equals together.

Programming Exercises

  1. Write a program CountVowels.java that reads a string and prints the number of vowels (a, e, i, o, u), ignoring case.
  2. Write a program WordCount.java that reads a sentence and prints the number of words (split on whitespace).
  3. Write a program Initials.java that reads a full name and prints the initials (e.g. "Mohammad Ali" → "M.A.").
  4. Write a program AnagramCheck.java that checks whether two strings are anagrams (same letters, reordered), ignoring case and non-letters.
  5. Write a program ReverseWords.java that reverses the order of words in a sentence (e.g. "Java is fun" → "fun is Java") using split and StringBuilder.
  6. Write a program DigitValidator.java that uses a regex to check whether a string contains exactly 11 digits (a phone number).

Part III — Object-Oriented Programming and Design

Chapter 7 — Introduction to Classes, Objects, Methods, and Strings

Object-oriented programming (OOP) means programming using objects. An object represents an entity that can be distinctly identified; it has a unique identity, a state (its properties or attributes, stored in data fields), and a behavior (its methods). A class is the template—or blueprint—that defines what an object's data fields and methods will be, and an object is an instance of a class. This chapter shows how to define classes, construct objects, and use them.

After studying this chapter you will be able to:

7.1 Defining a Class and Creating Objects

A class is essentially a programmer-defined type. You declare a class with the class keyword, give it a name, and place its fields and methods inside braces. For example, a Circle class has a radius field and getArea/getPerimeter methods.

An object reference variable holds a reference to an object (not the object itself). You create an object with new, which allocates memory and returns a reference:

Circle myCircle;            // declaration of a reference variable
myCircle = new Circle();    // creation: new allocates an object, returns its reference
Circle c = new Circle(25);  // declare, create, and assign in one statement

Accessing members. An object's members are its data fields and methods. You reach them with the dot operator (.), also called the object member access operator:

myCircle.radius = 5.0;          // access a field
double area = myCircle.getArea(); // invoke a method

The class that contains main is the main class (runnable); a class without main, like Circle, is just a definition and cannot be run on its own. To keep each example self-contained in one file, this book places the helper class (e.g. Circle) in the same file as the public main class.

Listing: TestCircle.java

// TestCircle.java — Defining a class, constructing objects, and using the dot operator.
public class TestCircle {
    public static void main(String[] args) {
        Circle c1 = new Circle();          // default constructor: radius 1
        Circle c2 = new Circle(25);        // constructor with an argument
        Circle c3 = new Circle(125);

        System.out.println("Area of c1 (r=" + c1.radius + ") = " + c1.getArea());
        System.out.println("Area of c2 (r=" + c2.radius + ") = " + c2.getArea());
        System.out.println("Perimeter of c3 (r=" + c3.radius + ") = " + c3.getPerimeter());

        c2.radius = 100;                   // modify a field via the dot operator
        System.out.println("Area of c2 (r=" + c2.radius + ") = " + c2.getArea());
    }
}

// A Circle class: a blueprint for circle objects. (package-private, no main)
class Circle {
    double radius;                         // data field

    /** Construct a circle with default radius 1. */
    Circle() {
        radius = 1;
    }

    /** Construct a circle with a specified radius. */
    Circle(double newRadius) {
        radius = newRadius;
    }

    /** Return the area of this circle. */
    double getArea() {
        return radius * radius * Math.PI;
    }

    /** Return the perimeter of this circle. */
    double getPerimeter() {
        return 2 * radius * Math.PI;
    }

    /** Set a new radius. */
    void setRadius(double newRadius) {
        radius = newRadius;
    }
}

Output:

Area of c1 (r=1.0) = 3.141592653589793
Area of c2 (r=25.0) = 1963.4954084936207
Perimeter of c3 (r=125.0) = 785.3981633974482
Area of c2 (r=100.0) = 31415.926535897932

The Circle class defines the form of a circle; each new Circle(...) creates an independent object with its own radius. The dot operator reads a field (c2.radius) and invokes a method (c2.getArea()).

7.2 Constructors

A constructor is a special method that initializes an object. It is invoked automatically when you use new. Constructors differ from ordinary methods in three ways:

A class can have multiple constructors (constructor overloading), as long as their parameter lists differ. The no-arg constructor Circle() gives a default radius of 1; Circle(double newRadius) lets the caller choose. If you define no constructors at all, Java provides an invisible no-arg constructor that initializes fields to their defaults.

Listing: TestStudent.java

// TestStudent.java — A class with a constructor that initializes fields.
public class TestStudent {
    public static void main(String[] args) {
        Student s1 = new Student("Alice", 20);
        Student s2 = new Student("Bob", 22);

        s1.displayInfo();
        s2.displayInfo();
    }
}

class Student {
    String name;
    int age;

    // Constructor: same name as the class, no return type.
    Student(String n, int a) {
        name = n;
        age = a;
    }

    void displayInfo() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

Output:

Name: Alice, Age: 20
Name: Bob, Age: 22

7.3 Reference Data Fields and null

A reference data field is a field whose type is a class (such as String). Such a field is null by default if you do not initialize it—meaning it refers to no object. Using null to access a member throws NullPointerException at runtime:

String s;          // s is null by default
// s.length();     // would throw NullPointerException

A field of a primitive type defaults to 0, false, or '\u0000'; a field of a reference type defaults to null. To avoid NullPointerException, initialize reference fields (often in a constructor) before using them.

7.4 Primitive vs. Reference Variables

A variable of a primitive type holds the value itself. A variable of a reference type holds a reference to an object. This difference matters most when you assign one variable to another:

Listing: ReferenceVsPrimitive.java

// ReferenceVsPrimitive.java — Primitive variables hold values; object variables hold references.
public class ReferenceVsPrimitive {
    public static void main(String[] args) {
        // Primitive: copying the value
        int a = 5;
        int b = a;
        b = 10;
        System.out.println("Primitive: a = " + a + ", b = " + b); // a=5, b=10

        // Reference: copying the reference -> two variables share one object
        Box box1 = new Box();
        box1.value = 5;
        Box box2 = box1;        // box2 now refers to the SAME object as box1
        box2.value = 10;
        System.out.println("Reference: box1.value = " + box1.value   // 10
            + ", box2.value = " + box2.value);

        // null: a reference variable that points to no object
        Box box3 = null;
        System.out.println("box3 is " + box3); // prints "null"
    }
}

class Box {
    int value;
}

Output:

Primitive: a = 5, b = 10
Reference: box1.value = 10, box2.value = 10
box3 is null

After b = a, changing b does not affect a. But after box2 = box1, changing box2.value does affect box1.value, because both names refer to the one Box object. Understanding this distinction is essential for the rest of the book.

Worked Example: A TV Class

A TV has state (channel, volume, on/off) and behavior (turn on/off, change channel, adjust volume). This program defines a TV class and drives two independent TV objects from main.

Listing: TestTV.java

// TestTV.java — Worked example for Chapter 7.
// A TV class with state (channel, volume, on/off) and behaviors, plus a main
// that creates two TVs and manipulates them independently.
public class TestTV {
    public static void main(String[] args) {
        TV tv1 = new TV();
        tv1.turnOn();
        tv1.setChannel(30);
        tv1.volumeUp();

        TV tv2 = new TV();
        tv2.turnOn();
        tv2.channelUp();
        tv2.channelUp();

        System.out.println("tv1: channel " + tv1.channel + ", volume " + tv1.volume);
        System.out.println("tv2: channel " + tv2.channel + ", volume " + tv2.volume);
    }
}

class TV {
    int channel = 1;
    int volume = 1;
    boolean on = false;

    void turnOn()  { on = true;  }
    void turnOff() { on = false; }

    void setChannel(int newChannel) {
        if (on && newChannel >= 1 && newChannel <= 120) {
            channel = newChannel;
        }
    }

    void channelUp() {
        if (on && channel < 120) channel++;
    }

    void channelDown() {
        if (on && channel > 1) channel--;
    }

    void volumeUp() {
        if (on && volume < 7) volume++;
    }

    void volumeDown() {
        if (on && volume > 1) volume--;
    }
}

Output:

tv1: channel 30, volume 2
tv2: channel 3, volume 1

Each method guards its action with if (on …) so that a turned-off TV ignores commands. The two TV objects are independent: tv1 ends at channel 30, volume 2, while tv2—created fresh and channel-upped twice from the default 1—ends at channel 3, volume 1.

Chapter Summary

Review Questions

  1. What three things characterize an object? Which part is the "state" and which is the "behavior"?
  2. What is the difference between a class and an instance?
  3. Why does a class like Circle with no main method not run on its own?
  4. List the three rules that distinguish a constructor from an ordinary method.
  5. What does the dot operator do? Give an example of accessing a field and invoking a method.
  6. What is the default value of an uninitialized String field? Of an int field? Of a boolean field?
  7. What exception do you get by calling a method on a null reference?
  8. In ReferenceVsPrimitive.java, why does changing box2.value also change box1.value?
  9. If a class defines no constructors, can you still write new ClassName()? Why?
  10. In TestTV.java, why does setChannel check on before changing the channel?

Programming Exercises

  1. Write a Rectangle class with width and height fields, two constructors (default 1×1 and a parameterized one), and getArea/getPerimeter methods. Add a TestRectangle main class.
  2. Write an Account class with a double balance field, a constructor that sets the initial balance, and deposit/withdraw methods. Add a TestAccount main class that deposits and withdraws.
  3. Write a Book class with title, author, and price fields and a constructor; include a display method. Test it with two books.
  4. Write a Fan class with speed (int), on (boolean), and radius (double) fields and methods to turn on/off and change speed. Demonstrate two fans.
  5. Write a Stock class with a symbol and a name, plus previousClosingPrice and currentPrice fields and a getChangePercent() method. Test it.
  6. Write a Stopwatch class with start/stop methods that record System.currentTimeMillis() and a getElapsedTime() method.

Chapter 8 — Classes and Objects: A Deeper Look

This chapter goes deeper into designing classes well. It covers static members, visibility modifiers and encapsulation, passing objects to methods, variable scope, the this reference, arrays of objects, immutable classes, processing primitives as objects using wrapper classes and BigInteger/BigDecimal, and the relationships among classes.

After studying this chapter you will be able to:

8.1 Static Variables, Constants, and Methods

An instance variable is tied to a specific instance; each object has its own copy. A static variable (also called a class variable) is shared by all instances—there is one copy in a common memory location. Use a static variable when all objects of a class should share data (for example, a count of how many objects have been created).

Add the static modifier to declare a static variable or method. A static method can be called through the class name (ClassName.method()) without creating an object, and it can access only static members directly (it has no this). Constants shared by all instances should be static final (for example, static final double PI = 3.14159;).

Listing: StaticDemo.java

// StaticDemo.java — Static (class) variables and methods are shared by all instances.
public class StaticDemo {
    public static void main(String[] args) {
        System.out.println("Before creating objects, numberOfObjects = "
            + CircleWithStaticMembers.numberOfObjects); // access via the class name

        CircleWithStaticMembers c1 = new CircleWithStaticMembers();      // radius 1
        CircleWithStaticMembers c2 = new CircleWithStaticMembers(5);     // radius 5

        c1.radius = 9;
        System.out.println("c1: radius = " + c1.radius + ", area = " + c1.getArea());
        System.out.println("c2: radius = " + c2.radius + ", area = " + c2.getArea());
        System.out.println("numberOfObjects = "
            + CircleWithStaticMembers.getNumberOfObjects());
    }
}

class CircleWithStaticMembers {
    double radius;
    static int numberOfObjects = 0;       // shared by all instances

    CircleWithStaticMembers() {
        radius = 1;
        numberOfObjects++;
    }

    CircleWithStaticMembers(double newRadius) {
        radius = newRadius;
        numberOfObjects++;
    }

    static int getNumberOfObjects() {     // static method: no object needed to call it
        return numberOfObjects;
    }

    double getArea() {
        return radius * radius * Math.PI;
    }
}

Output:

Before creating objects, numberOfObjects = 0
c1: radius = 9.0, area = 254.46900494077323
c2: radius = 5.0, area = 78.53981633974483
numberOfObjects = 2

Instance or static? If a property or behavior depends on a specific instance (like radius and getArea), make it an instance member. If it is shared by all instances or independent of any instance (like numberOfObjects or a math helper), make it static. main is static so the JVM can start the program without creating an object.

8.2 Visibility Modifiers

Visibility modifiers control access to a class and its members from outside the class:

Using public/private on local variables is a compile error; modifiers apply to members and (for public) to classes.

8.3 Data-Field Encapsulation

Letting outsiders modify data fields directly is risky: data can be tampered with, and the class becomes hard to maintain. Encapsulation hides the data by making fields private and exposing controlled access through getter (accessor) and setter (mutator) methods:

A setter can validate its argument and reject invalid values, which keeps an object's state always valid.

Listing: EncapsulationDemo.java

// EncapsulationDemo.java — Private fields with public getters/setters (data-field encapsulation).
public class EncapsulationDemo {
    public static void main(String[] args) {
        CirclePrivate c = new CirclePrivate(5);
        System.out.println("radius = " + c.getRadius());
        System.out.println("area   = " + c.getArea());

        c.setRadius(10);
        System.out.println("new radius = " + c.getRadius());
        System.out.println("new area   = " + c.getArea());

        // c.radius = -5; // compile error: radius is private
        c.setRadius(-5);   // the setter rejects the invalid value
        System.out.println("after setRadius(-5), radius = " + c.getRadius()); // still 10
    }
}

class CirclePrivate {
    private double radius = 1;
    private static int numberOfObjects = 0;

    public CirclePrivate() {
        numberOfObjects++;
    }

    public CirclePrivate(double newRadius) {
        setRadius(newRadius);   // route through the setter to validate
        numberOfObjects++;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double newRadius) {
        if (newRadius > 0) {     // reject non-positive values
            radius = newRadius;
        }
    }

    public static int getNumberOfObjects() {
        return numberOfObjects;
    }

    public double getArea() {
        return radius * radius * Math.PI;
    }
}

Output:

radius = 5.0
area   = 78.53981633974483
new radius = 10.0
new area   = 314.1592653589793
after setRadius(-5), radius = 10.0

c.radius = -5; would not compile because radius is private; c.setRadius(-5) compiles but the setter ignores the negative value, so the radius stays 10. This is the payoff of encapsulation: the object protects its own invariants.

8.4 Passing Objects to Methods and the Scope of Variables

When you pass an object (a reference type) to a method, the reference is passed by value—so the method receives a copy of the reference and can read or change the object's contents through it, although it cannot make the caller's variable refer to a different object. This is the object analog of the array behavior from Section 5.4.

The scope of instance and static variables is the whole class, regardless of where they are declared. The scope of a local variable runs from its declaration to the end of its enclosing block. A local variable shadows an instance variable of the same name; the keyword this (next section) lets you reach the shadowed field.

8.5 The this Reference

The keyword this refers to the object itself. Two common uses:

Listing: ThisDemo.java

// ThisDemo.java — Using `this` to refer to hidden fields and to call another constructor.
public class ThisDemo {
    public static void main(String[] args) {
        Person p1 = new Person();            // uses this("Unknown", 0)
        Person p2 = new Person("Alice");     // uses this(name, 0)
        Person p3 = new Person("Bob", 25);   // uses the (String, int) constructor

        p1.display();
        p2.display();
        p3.display();
    }
}

class Person {
    private String name;
    private int age;

    // No-arg constructor calls the (String, int) constructor via this(...)
    Person() {
        this("Unknown", 0);
    }

    Person(String name) {
        this(name, 0);     // this(...) must be the first statement
    }

    Person(String name, int age) {
        this.name = name;  // `this.name` is the field; `name` is the parameter
        this.age = age;
    }

    void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

Output:

Name: Unknown, Age: 0
Name: Alice, Age: 0
Name: Bob, Age: 25

The two simpler constructors delegate to the most specific one via this(...), so the initialization logic lives in exactly one place.

8.6 Immutable Objects and Classes

An object is immutable if its state cannot change after construction (like String). For a class to be immutable: make all data fields private, provide no mutators (setters), and ensure no method returns a reference to a mutable internal object (return a copy instead). Immutability makes objects simple, thread-safe, and safe to share.

8.7 Processing Primitives as Objects: Wrappers, BigInteger, BigDecimal

For performance, primitives (int, double, …) are not objects. But sometimes you need an object—generic collections, for instance, only hold objects. Java provides wrapper classes (Integer, Double, Boolean, Character, …) that wrap a primitive. Converting a primitive to a wrapper is boxing; the reverse is unboxing; Java does both automatically (autoboxing/unboxing). The wrapper classes also provide conversion helpers such as Integer.parseInt and Double.parseDouble.

For very large or high-precision numbers, java.math.BigInteger and java.math.BigDecimal offer arbitrary-precision arithmetic with no overflow (methods are called on the object, e.g. a.multiply(b)).

Listing: WrapperAndBigIntegerDemo.java

// WrapperAndBigIntegerDemo.java — Wrapper classes, autoboxing/unboxing, and BigInteger.
import java.math.BigInteger;

public class WrapperAndBigIntegerDemo {
    public static void main(String[] args) {
        // Wrapper objects (Integer, Double, ...) wrap primitives.
        Integer boxed = Integer.valueOf(42);  // explicit boxing
        int unboxed = boxed.intValue();        // explicit unboxing
        System.out.println("boxed = " + boxed + ", unboxed = " + unboxed);

        // Autoboxing/unboxing: Java converts automatically.
        Integer auto = 7;        // autobox int -> Integer
        int n = auto + 3;        // auto-unbox, add, result is int
        System.out.println("auto + 3 = " + n);

        // Numeric conversion helpers on the wrapper classes
        int parsed = Integer.parseInt("1024");
        double d = Double.parseDouble("3.14");
        String s = Integer.toString(99);
        System.out.println("parsed int = " + parsed + ", parsed double = " + d + ", str = " + s);

        // BigInteger: arbitrary-precision integers (no overflow)
        System.out.println("50! = " + factorial(50));
    }

    public static BigInteger factorial(long n) {
        BigInteger result = BigInteger.ONE;
        for (int i = 1; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result;
    }
}

Output:

boxed = 42, unboxed = 42
auto + 3 = 10
parsed int = 1024, parsed double = 3.14, str = 99
50! = 30414093201713378043612608166064768844377641568960512000000000000

50! far exceeds long's range (about 9.2 × 10¹⁸), so it would overflow ordinary integer arithmetic; BigInteger computes its exact 65-digit value.

8.8 Class Relationships

Classes relate in three common ways:

Worked Example: Total Area of an Array of Circles

An array can hold objects just as it holds primitives—but an array of objects is actually an array of references. This program builds an array of five circles with random radii and sums their areas, combining array-of-objects processing with encapsulation.

Listing: TotalArea.java

// TotalArea.java — Worked example for Chapter 8.
// Creates an array of Circle objects with random radii and computes the total area,
// demonstrating arrays of objects together with encapsulation.
public class TotalArea {
    public static void main(String[] args) {
        CircleForTotal[] circleArray = createCircleArray(5);
        printCircleArray(circleArray);
    }

    /** Create an array of n circles with random radii in [1, 10). */
    public static CircleForTotal[] createCircleArray(int n) {
        CircleForTotal[] arr = new CircleForTotal[n];
        for (int i = 0; i < n; i++) {
            arr[i] = new CircleForTotal(1 + Math.random() * 9);
        }
        return arr;
    }

    /** Print each circle's radius and area, and the sum of the areas. */
    public static void printCircleArray(CircleForTotal[] arr) {
        System.out.printf("%-10s%-15s%n", "Radius", "Area");
        double sum = 0;
        for (CircleForTotal c : arr) {
            System.out.printf("%-10.4f%-15.4f%n", c.getRadius(), c.getArea());
            sum += c.getArea();
        }
        System.out.printf("%-10s%-15.4f%n", "Total", sum);
    }
}

class CircleForTotal {
    private double radius = 1;

    public CircleForTotal() {}

    public CircleForTotal(double newRadius) {
        setRadius(newRadius);
    }

    public double getRadius() { return radius; }

    public void setRadius(double newRadius) {
        if (newRadius > 0) radius = newRadius;
    }

    public double getArea() { return radius * radius * Math.PI; }
}

One sample run (radii are random, so output varies):

Radius    Area
6.1624    119.3019
8.8891    248.2358
4.1730    54.7082
8.9691    252.7248
1.0883    3.7206
Total     678.6912

new CircleForTotal[n] creates an array of n null references; the loop replaces each null with a freshly constructed object. Each element is accessed two ways: circleArray references the whole array, and circleArray[i] (or c in the foreach) references a CircleForTotal object whose getArea() is then called.

Chapter Summary

Review Questions

  1. What is the difference between an instance variable and a static variable? Which is shared by all objects?
  2. How do you invoke a static method without creating an object? Give an example.
  3. When should a member be static rather than instance? Give two examples of each.
  4. What are the three visibility levels, and what does each permit?
  5. Why is direct field access from outside a class discouraged? How do getters/setters help?
  6. In EncapsulationDemo.java, what happens when you call setRadius(-5), and why?
  7. Give two uses of the this keyword. Why must this(args) be the first statement in a constructor?
  8. What does it mean for a class to be immutable? List the rules for making a class immutable.
  9. What is autoboxing and unboxing? Why are wrapper classes needed for generic collections?
  10. An array of objects created with new Circle[n] holds n objects or n references? What must you do before using an element?

Programming Exercises

  1. Add a static counter to the Rectangle class from Exercise 7.1 that tracks how many rectangles have been created, and a static method getNumberOfRectangles() to read it.
  2. Make the Account class from Exercise 7.2 fully encapsulated: private balance, a getBalance() accessor, and a deposit/withdraw that validate amounts. Reject overdrafts.
  3. Write a Time class with private hour, minute, second fields, a constructor, and a toString(). Use this(...) to chain two constructors.
  4. Write a program that stores 10 random Integer values in an array, autoboxes them, then computes and prints their sum and average.
  5. Use BigInteger to compute and print 100!.
  6. Write a program that builds an array of 5 Book objects (encapsulated) and prints the most expensive book.

Chapter 9 — Inheritance

Inheritance lets you define a general class that captures common properties and behaviors, then extend it with specialized subclasses. A class C1 extended from C2 is a subclass, and C2 is its superclass (also called parent or base class). The subclass inherits the superclass's accessible fields and methods and can add its own. This chapter covers defining inheritance, the super keyword, method overriding versus overloading, and the Object class and toString.

After studying this chapter you will be able to:

9.1 Superclasses and Subclasses

Different classes often share common properties and behaviors. Inheritance lets you factor the common parts into a superclass and specialize them in subclasses. A subclass is not a subset of its superclass—on the contrary, a subclass usually extends the superclass with more information and methods. The keyword extends declares the relationship:

class Dog extends Animal { ... }

Dog inherits Animal's accessible (non-private) members and can add fields (like breed) and methods (like bark). private members of the superclass are not directly accessible in the subclass; use public/protected accessors instead. Java allows single inheritance—a class can extend only one superclass—but a class can implement many interfaces (Chapter 10).

Listing: SuperAndSubclass.java

// SuperAndSubclass.java — A subclass inherits fields and methods from its superclass
// and can add its own. The keyword `extends` declares the inheritance relationship.
public class SuperAndSubclass {
    public static void main(String[] args) {
        Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
        myDog.eat();        // inherited from Animal
        myDog.bark();       // Dog's own method
        System.out.println(myDog); // uses Dog's toString
    }
}

class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return "Animal[name=" + name + ", age=" + age + "]";
    }
}

class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);   // call the superclass constructor
        this.breed = breed;
    }

    public void bark() {
        System.out.println(getName() + " says: Woof!");
    }

    @Override
    public String toString() {
        return "Dog[name=" + getName() + ", age=" + getAge() + ", breed=" + breed + "]";
    }
}

Output:

Buddy is eating.
Buddy says: Woof!
Dog[name=Buddy, age=3, breed=Golden Retriever]

Dog reuses Animal's eat and the getName/getAge accessors, adds bark, and overrides toString.

9.2 Using the super Keyword

The super keyword refers to the superclass. Two uses:

Listing: SuperKeywordDemo.java

// SuperKeywordDemo.java — Using `super` to call a superclass constructor and methods.
public class SuperKeywordDemo {
    public static void main(String[] args) {
        Car myCar = new Car("Blue", 4, 200);
        myCar.display();
    }
}

class Vehicle {
    protected String color;

    public Vehicle(String color) {
        this.color = color;
    }

    public void start() {
        System.out.println("The " + color + " vehicle is starting.");
    }
}

class Car extends Vehicle {
    private int wheels;
    private int topSpeed;

    public Car(String color, int wheels, int topSpeed) {
        super(color);          // call Vehicle(String)
        this.wheels = wheels;
        this.topSpeed = topSpeed;
    }

    public void display() {
        super.start();         // call the superclass method
        System.out.println("It has " + wheels + " wheels and a top speed of "
            + topSpeed + " km/h.");
    }
}

Output:

The Blue vehicle is starting.
It has 4 wheels and a top speed of 200 km/h.

The protected modifier (Section 8.2 context) makes color accessible to subclasses (and the package). super(color) initializes it via Vehicle's constructor; super.start() reuses the parent's behavior from within Car.

9.3 Method Overriding

Method overriding occurs when a subclass provides its own implementation of a method already defined in the superclass. Rules:

Listing: OverridingDemo.java

// OverridingDemo.java — A subclass provides its own implementation of an inherited method.
public class OverridingDemo {
    public static void main(String[] args) {
        Pet p1 = new Cat("Mimi");
        Pet p2 = new Cow("Bessie");

        p1.makeSound();   // "Mimi meows: Meow!"
        p2.makeSound();   // "Bessie moos: Moo!"
        System.out.println(p1);
        System.out.println(p2);
    }
}

class Pet {
    protected String name;

    public Pet(String name) {
        this.name = name;
    }

    public void makeSound() {
        System.out.println(name + " makes a sound.");
    }

    @Override
    public String toString() {
        return "Pet[" + name + "]";
    }
}

class Cat extends Pet {
    public Cat(String name) { super(name); }

    @Override
    public void makeSound() {
        System.out.println(name + " meows: Meow!");
    }

    @Override
    public String toString() {
        return "Cat[" + name + "]";
    }
}

class Cow extends Pet {
    public Cow(String name) { super(name); }

    @Override
    public void makeSound() {
        System.out.println(name + " moos: Moo!");
    }

    @Override
    public String toString() {
        return "Cow[" + name + "]";
    }
}

Output:

Mimi meows: Meow!
Bessie moos: Moo!
Cat[Mimi]
Cow[Bessie]

Cat and Cow each override makeSound and toString with their own behavior. (Which version runs is decided at runtime by the object's actual type—this is dynamic binding, the foundation of polymorphism in Chapter 10.)

9.4 Overriding vs. Overloading

These two are easy to confuse but are distinct:

Listing: OverridingVsOverloading.java

// OverridingVsOverloading.java — Overriding redefines an inherited method (same signature);
// overloading defines same-named methods with different parameter lists in the same class.
public class OverridingVsOverloading {
    public static void main(String[] args) {
        Greeting g = new Greeting();
        FormalGreeting f = new FormalGreeting();

        g.sayHello();       // "Hello!"
        f.sayHello();       // "Good day to you!"  <- overriding
        g.sayHello(3);      // says hello 3 times  <- overloading
        f.sayHello(2);      // inherited overloaded method
    }
}

class Greeting {
    public void sayHello() {
        System.out.println("Hello!");
    }

    // Overloaded: same name, different parameter list (same class).
    public void sayHello(int times) {
        for (int i = 0; i < times; i++) {
            System.out.println("Hello!");
        }
    }
}

class FormalGreeting extends Greeting {
    @Override                 // overriding: same signature as Greeting.sayHello()
    public void sayHello() {
        System.out.println("Good day to you!");
    }
}

Output:

Hello!
Good day to you!
Hello!
Hello!
Hello!
Hello!
Hello!

Greeting overloads sayHello (no-arg and int versions in the same class). FormalGreeting overrides the no-arg sayHello but inherits the overloaded sayHello(int), which is why f.sayHello(2) prints Hello! twice.

9.5 The Object Class and toString

Every Java class implicitly extends java.lang.Object if it extends nothing else, so Object is the common root of the class hierarchy. Useful Object methods include toString(), equals(Object), and hashCode(). System.out.println(obj) calls obj.toString() automatically, so overriding toString controls how your object prints. The default Object.toString returns something like ClassName@hexHashCode; overriding it to show the object's state is good practice (all the examples above do this). The @Override annotation tells the compiler to verify that toString really overrides the inherited version.

Worked Example: A Geometric-Object Hierarchy

This example models a small inheritance hierarchy: a superclass SimpleGeometricObject (with color, filled, and a creation date, plus a toString) is extended by GeometricCircle and GeometricRectangle, each adding geometry methods and overriding toString while reusing the parent's version via super.toString().

Listing: GeometricHierarchy.java

// GeometricHierarchy.java — Worked example for Chapter 9.
// A superclass SimpleGeometricObject holds color/filled/date, with Circle and Rectangle
// subclasses that add their own geometry and override toString().
public class GeometricHierarchy {
    public static void main(String[] args) {
        GeometricCircle c = new GeometricCircle(5);
        c.setColor("red");
        c.setFilled(true);

        GeometricRectangle r = new GeometricRectangle(2, 3);
        r.setColor("blue");
        r.setFilled(false);

        System.out.println(c);
        System.out.println("  area = " + c.getArea() + ", perimeter = " + c.getPerimeter());
        System.out.println(r);
        System.out.println("  area = " + r.getArea() + ", perimeter = " + r.getPerimeter());
    }
}

class SimpleGeometricObject {
    private String color = "white";
    private boolean filled;
    private java.util.Date dateCreated = new java.util.Date();

    public SimpleGeometricObject() {}

    public String getColor() { return color; }
    public void setColor(String color) { this.color = color; }
    public boolean isFilled() { return filled; }
    public void setFilled(boolean filled) { this.filled = filled; }
    public java.util.Date getDateCreated() { return dateCreated; }

    @Override
    public String toString() {
        return "created on " + dateCreated + "\ncolor: " + color + " and filled: " + filled;
    }
}

class GeometricCircle extends SimpleGeometricObject {
    private double radius;

    public GeometricCircle() {}
    public GeometricCircle(double radius) { this.radius = radius; }

    public double getRadius() { return radius; }
    public void setRadius(double radius) { this.radius = radius; }
    public double getArea() { return radius * radius * Math.PI; }
    public double getPerimeter() { return 2 * radius * Math.PI; }

    @Override
    public String toString() {
        return "Circle\n" + super.toString() + "\nradius = " + radius;
    }
}

class GeometricRectangle extends SimpleGeometricObject {
    private double width;
    private double height;

    public GeometricRectangle() {}
    public GeometricRectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() { return width; }
    public void setWidth(double width) { this.width = width; }
    public double getHeight() { return height; }
    public void setHeight(double height) { this.height = height; }
    public double getArea() { return width * height; }
    public double getPerimeter() { return 2 * (width + height); }

    @Override
    public String toString() {
        return "Rectangle\n" + super.toString()
            + "\nwidth = " + width + ", height = " + height;
    }
}

Sample output (the creation date reflects when the program runs):

Circle
created on Thu Sep 17 21:02:37 BDT 2026
color: red and filled: true
radius = 5.0
  area = 78.53981633974483, perimeter = 31.41592653589793
Rectangle
created on Thu Sep 17 21:02:37 BDT 2026
color: blue and filled: false
width = 2.0, height = 3.0
  area = 6.0, perimeter = 10.0

Both subclasses inherit getColor/setColor/isFilled/setFilled and add their own getArea/getPerimeter. Their toString prepends the shape name, calls super.toString() to reuse the parent's "created on … color … filled …" text, then appends the shape-specific dimensions.

Chapter Summary

Review Questions

  1. What keyword declares that one class extends another? Which class is the superclass and which is the subclass?
  2. Can a subclass access a private field of its superclass directly? How should it reach that data?
  3. Java allows a class to extend how many superclasses? What does this imply about multiple inheritance?
  4. What are the two uses of super? Why must super(args) be the first statement?
  5. State the rules a method must follow to correctly override a superclass method.
  6. What does the @Override annotation do, and why use it?
  7. Give a one-sentence definition each of overriding and overloading. How do you tell them apart?
  8. In OverridingVsOverloading.java, why does f.sayHello(2) print Hello! and not Good day to you!?
  9. Which class is the root of the Java class hierarchy? Name two methods it provides.
  10. What does System.out.println(obj) do with obj, and how does overriding toString affect it?

Programming Exercises

  1. Write a Person superclass with name and address, and a Student subclass that adds studentId and a toString override. Test it.
  2. Write a BankAccount superclass with balance and deposit/withdraw, and a SavingsAccount subclass that adds an interestRate and an addInterest() method.
  3. Write a Shape superclass with a getArea() method returning 0, and Circle and Square subclasses that override getArea().
  4. Write a Manager subclass of Employee (with name and salary) that adds a bonus field and overrides a getIncome() method.
  5. Write a program that defines an Animal superclass and at least three subclasses, each overriding a sound() method, and demonstrates them in main.
  6. Add a equals(Object) override to the Person class from Exercise 1 that compares two persons by name and address.

Chapter 10 — Polymorphism and Interfaces

Polymorphism (Greek: "many forms") means that one reference type can refer to many actual object types, and the right behavior is chosen at runtime. Abstract classes and interfaces are the two mechanisms Java gives you for abstraction—hiding implementation details and exposing only essential functionality. This chapter ties inheritance (Chapter 9) to polymorphism, then introduces abstract classes and interfaces.

After studying this chapter you will be able to:

10.1 Polymorphism and Dynamic Binding

A variable of a superclass (or interface) type can refer to an object of any subclass (or implementer)—this is polymorphism. When you call an overridden method through such a variable, the JVM chooses the version that matches the object's actual runtime type, not the variable's declared type. This runtime choice is dynamic binding (also called dynamic method dispatch).

Listing: PolymorphismDemo.java

// PolymorphismDemo.java — A superclass variable can refer to a subclass object; the
// overridden method that actually runs is chosen by the object's real type (dynamic binding).
public class PolymorphismDemo {
    public static void main(String[] args) {
        AnimalPoly a1 = new DogPoly();   // upcast: a Dog is an Animal
        AnimalPoly a2 = new CatPoly();
        a1.sound();   // "Woof!"  <- Dog's version
        a2.sound();   // "Meow!"  <- Cat's version
    }
}

class AnimalPoly {
    public void sound() {
        System.out.println("Some animal sound");
    }
}

class DogPoly extends AnimalPoly {
    @Override
    public void sound() { System.out.println("Woof!"); }
}

class CatPoly extends AnimalPoly {
    @Override
    public void sound() { System.out.println("Meow!"); }
}

Output:

Woof!
Meow!

Both a1 and a2 are declared AnimalPoly, yet a1.sound() prints Woof! and a2.sound() prints Meow!. The compiler only verifies that AnimalPoly has a sound method; the JVM decides at runtime that the Dog and Cat overrides should run. This is what lets a single loop over AnimalPoly[] call each animal's own sound.

10.2 Casting Objects and instanceof

Casting an object reference goes in two directions:

Listing: CastingDemo.java

// CastingDemo.java — Upcasting (implicit) and downcasting (explicit) with instanceof.
public class CastingDemo {
    public static void main(String[] args) {
        // Upcasting: a subclass object assigned to a superclass variable (implicit).
        Fruit f = new Apple("Fuji");
        f.describe();          // Apple's overridden describe()

        // Downcasting: a superclass variable cast back to a subclass type (explicit).
        if (f instanceof Apple) {
            Apple a = (Apple) f;     // explicit downcast
            a.peel();                // Apple-specific method
        }

        // A bad downcast would throw ClassCastException at runtime; guard with instanceof.
        Fruit b = new Banana("Cavendish");
        if (b instanceof Apple) {     // false
            Apple bad = (Apple) b;
            bad.peel();
        } else {
            System.out.println("b is not an Apple");
        }
    }
}

class Fruit {
    protected String name;
    public Fruit(String name) { this.name = name; }
    public void describe() { System.out.println("A fruit called " + name); }
}

class Apple extends Fruit {
    public Apple(String name) { super(name); }
    @Override
    public void describe() { System.out.println("An apple called " + name); }
    public void peel() { System.out.println("Peeling the apple " + name); }
}

class Banana extends Fruit {
    public Banana(String name) { super(name); }
    @Override
    public void describe() { System.out.println("A banana called " + name); }
}

Output:

An apple called Fuji
Peeling the apple Fuji
b is not an Apple

f.describe() runs Apple's version (dynamic binding). To call the Apple-specific peel() through a Fruit variable, we downcast with (Apple) f—but only after checking f instanceof Apple, avoiding a ClassCastException for the Banana.

10.3 Abstract Classes

Abstraction hides implementation details and shows only essential functionality. An abstract class cannot be instantiated and is meant to be subclassed; it can contain abstract methods (declared without a body) alongside concrete methods and fields. A concrete subclass must implement all abstract methods (or itself be abstract). Abstract classes provide partial abstraction: a base plus shared implementation.

Use the abstract modifier on the class and on each method without a body. Abstract classes can have constructors (called via super(...) from subclasses); they just cannot be new'd directly.

Listing: AbstractClassDemo.java

// AbstractClassDemo.java — An abstract class cannot be instantiated; it can have abstract
// methods (no body) and concrete methods. Subclasses implement the abstract methods.
public class AbstractClassDemo {
    public static void main(String[] args) {
        // ShapeA s = new ShapeA("red"); // error: ShapeA is abstract
        ShapeA c = new CircleA("red", 5);
        ShapeA r = new RectangleA("blue", 2, 3);
        System.out.println(c.getColor() + " circle area    = " + c.area());
        System.out.println(r.getColor() + " rectangle area = " + r.area());
    }
}

abstract class ShapeA {
    protected String color;

    public ShapeA(String color) {     // abstract classes CAN have constructors
        this.color = color;
    }

    public String getColor() { return color; }   // concrete method

    public abstract double area();               // abstract method: no body

    @Override
    public abstract String toString();
}

class CircleA extends ShapeA {
    private double radius;
    public CircleA(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    @Override
    public double area() { return radius * radius * Math.PI; }
    @Override
    public String toString() { return "Circle[r=" + radius + ", color=" + color + "]"; }
}

class RectangleA extends ShapeA {
    private double length, width;
    public RectangleA(String color, double length, double width) {
        super(color);
        this.length = length;
        this.width = width;
    }
    @Override
    public double area() { return length * width; }
    @Override
    public String toString() {
        return "Rectangle[" + length + "x" + width + ", color=" + color + "]";
    }
}

Output:

red circle area    = 78.53981633974483
blue rectangle area = 6.0

ShapeA provides the shared color field and a concrete getColor, but leaves area() and toString() abstract—each shape computes area differently. CircleA and RectangleA implement them. You cannot write new ShapeA("red") because the class is abstract.

10.4 Interfaces

An interface is a contract: a set of method signatures (and optionally constants) that a class promises to implement. A class declares that it implements an interface with the implements keyword and must provide bodies for the interface's abstract methods. Interfaces give full abstraction of behavior and, unlike classes, a class can implement many interfaces.

By default, methods in an interface are public and abstract, and all fields are public static final (constants). Modern Java (Java 8+) also allows interfaces to contain default methods (with a body) and static methods, so interfaces can evolve without breaking existing implementers.

Listing: InterfaceDemo.java

// InterfaceDemo.java — An interface defines a contract of abstract methods; classes
// implement it with `implements`. A variable of the interface type can refer to any implementer.
public class InterfaceDemo {
    public static void main(String[] args) {
        ShapeI c = new CircleI(5);        // interface reference, concrete object
        ShapeI r = new RectangleI(4, 6);
        System.out.println("Circle area    = " + c.calculateArea());
        System.out.println("Rectangle area = " + r.calculateArea());
    }
}

interface ShapeI {
    double calculateArea();   // implicitly public and abstract
    // All fields in an interface are implicitly public static final (constants).
}

class CircleI implements ShapeI {
    private double radius;
    public CircleI(double radius) { this.radius = radius; }
    @Override
    public double calculateArea() { return radius * radius * Math.PI; }
}

class RectangleI implements ShapeI {
    private double length, width;
    public RectangleI(double length, double width) {
        this.length = length;
        this.width = width;
    }
    @Override
    public double calculateArea() { return length * width; }
}

Output:

Circle area    = 78.53981633974483
Rectangle area = 24.0

A ShapeI variable can refer to any class that implements ShapeI (here CircleI or RectangleI)—another form of polymorphism, this time across the interface type. A class can implement several interfaces (e.g., class X implements A, B), and an interface can extend other interfaces.

10.5 Abstract Class vs. Interface

Aspect Abstract class Interface
Variables/fields Any type and access modifier All public static final (constants)
Constructors Yes (typically protected, called via super) No
Methods Abstract and concrete Abstract, plus default/static methods
Inheritance A class extends one abstract class A class implements many interfaces
Instantiated? No No
Use when Sharing code + a common base Defining a role or capability across unrelated classes

Choose an abstract class when subclasses share code and a common "is-a" base; choose an interface when unrelated classes should share a capability (for example, Comparable, AutoCloseable, Runnable).

Worked Example: Polymorphic Area of an Array of Shapes

This example combines an abstract class with a polymorphic array. A method sumAreas takes a ShapeS[] and sums the areas; for each element the subclass's area() runs because of dynamic binding.

Listing: ShapeAreaSum.java

// ShapeAreaSum.java — Worked example for Chapter 10.
// An abstract Shape with subclasses, plus a method that sums the areas of an array
// of Shapes polymorphically — the right area() runs for each element via dynamic binding.
public class ShapeAreaSum {
    public static void main(String[] args) {
        ShapeS[] shapes = {
            new CircleS(2),
            new RectangleS(3, 4),
            new CircleS(5)
        };
        System.out.printf("Total area = %.2f%n", sumAreas(shapes));
    }

    public static double sumAreas(ShapeS[] shapes) {
        double total = 0;
        for (ShapeS s : shapes) {
            total += s.area();   // dynamic binding picks each subclass's area()
        }
        return total;
    }
}

abstract class ShapeS {
    public abstract double area();
}

class CircleS extends ShapeS {
    private double radius;
    public CircleS(double radius) { this.radius = radius; }
    @Override
    public double area() { return radius * radius * Math.PI; }
}

class RectangleS extends ShapeS {
    private double width, height;
    public RectangleS(double width, double height) {
        this.width = width;
        this.height = height;
    }
    @Override
    public double area() { return width * height; }
}

Output:

Total area = 103.11

The array holds mixed CircleS and RectangleS objects behind a ShapeS[] reference. sumAreas knows only ShapeS; it never branches on the concrete type, yet the correct area() runs for each element—12.57 (r=2) + 12 (3×4) + 78.54 (r=5) = 103.11. This is the payoff of polymorphism: code written to the abstraction works for any present and future subclass.

Chapter Summary

Review Questions

  1. What is polymorphism, and how does dynamic binding decide which overridden method runs?
  2. In PolymorphismDemo.java, both variables are declared AnimalPoly. Why do they print different sounds?
  3. Distinguish upcasting from downcasting. Which is implicit and which can throw an exception?
  4. Why do we check instanceof before a downcast? What exception occurs on a bad downcast?
  5. Can you instantiate an abstract class? Can it have constructors? Can it have concrete methods?
  6. What must a concrete subclass do with the abstract methods of its abstract superclass?
  7. What are the default access modifiers of methods and fields in an interface?
  8. Can a class implement more than one interface? Can an interface extend another interface?
  9. Give two differences between an abstract class and an interface.
  10. In ShapeAreaSum.java, why does sumAreas not need to know whether each shape is a circle or a rectangle?

Programming Exercises

  1. Add a Triangle subclass to the ShapeS hierarchy and include one in the shapes array of ShapeAreaSum.
  2. Define an interface Resizable with a method resize(double factor); make CircleS implement it so the radius scales by factor.
  3. Write an abstract class Employee with an abstract earnings() method and concrete name/toString; add SalariedEmployee and HourlyEmployee subclasses, and loop over an Employee[] printing each one's earnings polymorphically.
  4. Define an interface Comparable-like MyComparable with int compareTo(Object o) and implement it on a Circle class (compare by radius).
  5. Create an interface Edible with String howToEat(); implement it in Apple and Orange classes, and loop over an Edible[] printing how to eat each.
  6. Write a main that builds an array of Object containing a String, an Integer, and a custom Circle; use instanceof to print each element's type and value.

Chapter 11 — Exception Handling: A Deeper Look

A runtime error occurs while a program is running when the JVM detects an operation it cannot carry out—dividing by zero, accessing an array out of bounds, or parsing bad input. Java represents such errors as exceptions: objects that carry information about what went wrong and that can be caught so the program keeps running instead of crashing. Exception handling separates detecting an error (in a called method) from handling it (in the caller).

After studying this chapter you will be able to:

11.1 Exception-Handling Overview

Without exceptions, a method that hits a runtime error simply crashes. With exceptions, the method throws an exception object and a caller that catches it can decide what to do. The key benefit is the separation of detection from handling: a low-level method detects the problem; the high-level caller decides whether to recover or report.

Listing: QuotientWithException.java

// QuotientWithException.java — Handling a divide-by-zero with try/catch.
import java.util.Scanner;

public class QuotientWithException {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter two integers: ");
        int n1 = input.nextInt();
        int n2 = input.nextInt();

        try {
            int result = n1 / n2;
            System.out.println(n1 + " / " + n2 + " = " + result);
        } catch (ArithmeticException ex) {
            System.out.println("Exception: an integer cannot be divided by zero.");
        } finally {
            System.out.println("Execution continues after the try-catch.");
        }
        input.close();
    }
}

Two runs:

Enter two integers: 10 2
10 / 2 = 5
Execution continues after the try-catch.

Enter two integers: 10 0
Exception: an integer cannot be divided by zero.
Execution continues after the try-catch.

When n2 is 0, n1 / n2 throws an ArithmeticException; the matching catch block runs, and—crucially—execution continues after the try-catch instead of terminating. The finally block runs in both cases.

11.2 Exception Types

Exceptions are objects whose classes inherit from java.lang.Throwable. The hierarchy has two main branches:

Checked vs. unchecked. RuntimeException, Error, and their subclasses are unchecked exceptions—the compiler does not force you to catch or declare them (they usually reflect logic errors). Every other subclass of Exception is checked—the compiler requires you to either catch it or declare it with throws. You create your own exceptions by extending Exception (checked) or RuntimeException (unchecked).

11.3 Declaring, Throwing, and Catching Exceptions

Three operations form Java's exception model:

Listing: MultipleCatchDemo.java

// MultipleCatchDemo.java — Multiple catch blocks handle different exception types.
// Only the first matching catch runs; here list[5] throws first.
public class MultipleCatchDemo {
    public static void main(String[] args) {
        int[] list = {10, 20, 30};
        try {
            int index = 5;                  // out of bounds -> ArrayIndexOutOfBoundsException
            int value = list[index];        // throws here
            int result = value / 0;         // (unreached) would throw ArithmeticException
            System.out.println("result = " + result);
        } catch (ArrayIndexOutOfBoundsException ex) {
            System.out.println("Caught: array index out of bounds - " + ex.getMessage());
        } catch (ArithmeticException ex) {
            System.out.println("Caught: arithmetic error - " + ex.getMessage());
        }
    }
}

Output:

Caught: array index out of bounds - Index 5 out of bounds for length 3

list[5] throws first, so the ArrayIndexOutOfBoundsException catch runs and the division by zero is never reached. ex.getMessage() returns the detail message stored in the exception. From Java 7 you can also combine types in one catch: catch (IOException | SQLException ex).

11.4 The finally Block

finally runs whether the try completed normally, threw an exception that was caught, or threw one that was not caught (in which case finally still runs before the exception propagates). It is the right place for cleanup—closing a file or a scanner—that must happen regardless of outcome.

Listing: FinallyDemo.java

// FinallyDemo.java — The finally block always runs, whether or not an exception occurred.
import java.util.Scanner;

public class FinallyDemo {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a number: ");
        try {
            int n = input.nextInt();
            System.out.println("100 / " + n + " = " + (100 / n));
        } catch (ArithmeticException ex) {
            System.out.println("Cannot divide by zero.");
        } finally {
            System.out.println("finally: this runs no matter what.");
            input.close();
        }
    }
}

Two runs:

Enter a number: 5
100 / 5 = 20
finally: this runs no matter what.

Enter a number: 0
Cannot divide by zero.
finally: this runs no matter what.

In both runs the finally message appears.

11.5 Custom Exceptions

You define your own exception by extending Exception (a checked exception) or RuntimeException (unchecked). Pass a descriptive message to super(message) so getMessage() returns it. A method that may throw a checked exception must declare it with throws, and callers must either catch it or declare it themselves.

Listing: CustomExceptionDemo.java

// CustomExceptionDemo.java — Defining and using a custom checked exception.
import java.util.Scanner;

public class CustomExceptionDemo {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter your age: ");
        int age = input.nextInt();
        input.close();

        try {
            checkAge(age);
            System.out.println("Access granted.");
        } catch (InvalidAgeException ex) {
            System.out.println("Access denied: " + ex.getMessage());
        }
    }

    // A method that DECLARES it may throw a checked exception ("throws").
    public static void checkAge(int age) throws InvalidAgeException {
        if (age < 18) {
            throw new InvalidAgeException("Age must be at least 18.");
        }
    }
}

// A custom checked exception: extends Exception.
class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}

Two runs:

Enter your age: 15
Access denied: Age must be at least 18.

Enter your age: 20
Access granted.

checkAge declares throws InvalidAgeException because it is a checked exception; main catches it. If main did not catch it, main would itself have to declare throws InvalidAgeException.

Worked Example: A Robust Calculator

This program reads two integers and divides them. It can fail in two ways: bad input (NumberFormatException from Integer.parseInt) or division by zero (ArithmeticException). A separate catch handles each, and finally always prints a closing message.

Listing: RobustCalculator.java

// RobustCalculator.java — Worked example for Chapter 11.
// Reads two integers and divides them, handling bad input and divide-by-zero,
// with a finally block that always runs.
import java.util.Scanner;

public class RobustCalculator {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        try {
            System.out.print("Enter the first integer: ");
            int a = Integer.parseInt(input.nextLine().trim());
            System.out.print("Enter the second integer: ");
            int b = Integer.parseInt(input.nextLine().trim());
            System.out.printf("%d / %d = %d%n", a, b, a / b);
        } catch (NumberFormatException ex) {
            System.out.println("Error: that was not a valid integer.");
        } catch (ArithmeticException ex) {
            System.out.println("Error: cannot divide by zero.");
        } finally {
            System.out.println("Thank you for using the calculator.");
            input.close();
        }
    }
}

Three sample runs:

Enter the first integer: 10
Enter the second integer: 2
10 / 2 = 5
Thank you for using the calculator.

Enter the first integer: 10
Enter the second integer: 0
Error: cannot divide by zero.
Thank you for using the calculator.

Enter the first integer: abc
Error: that was not a valid integer.
Thank you for using the calculator.

Each failure mode is handled by its own catch, and the finally block thanks the user in every case—good input, divide-by-zero, and non-numeric input alike.

Chapter Summary

Review Questions

  1. What is the benefit of separating error detection from error handling?
  2. Name the root class of all exceptions and its two main branches.
  3. Give two examples of unchecked exceptions and one example of a checked exception.
  4. What is the difference between throw and throws?
  5. In a try with several catch clauses, how many catch blocks run when an exception occurs?
  6. Does the finally block run if the try throws an exception that is not caught? If the try completes normally?
  7. Why must a method that can throw a checked exception declare it with throws?
  8. How do you create a custom checked exception? How do you store a message in it?
  9. In RobustCalculator, which catch runs when the user types abc, and which when the user divides by zero?
  10. Why is it a bad idea to catch Exception broadly and ignore it (an empty catch)?

Programming Exercises

  1. Write a program that reads an array index from the user and prints list[index], catching ArrayIndexOutOfBoundsException.
  2. Write a method sqrt(double x) that throws an IllegalArgumentException if x is negative; call it from main inside a try-catch.
  3. Write a program that reads an integer with Integer.parseInt and catches NumberFormatException, printing a friendly message and re-prompting.
  4. Create a custom InvalidRadiusException and a Circle constructor that throws it for a negative radius. Demonstrate catching it.
  5. Write a program with a try-catch-finally where the try throws an exception that is not caught; observe that finally still runs before the program terminates.
  6. Write a program that divides two numbers and uses a multi-catch (catch (ArithmeticException | NumberFormatException ex)) to handle both errors with one block.

Chapter 12 — Files, Streams, and Object Serialization

I/O (Input/Output) is the transfer of data between a program and the outside world—files, the keyboard, the screen, or a network. Java models all of this as streams: sequences of data flowing between a source and a destination. This chapter covers text (character) I/O, binary (byte) I/O, buffered streams, DataInputStream/DataOutputStream for primitive values, and object serialization for saving whole objects to a file and reading them back.

After studying this chapter you will be able to:

12.1 Text vs. Binary Data

At the hardware level everything is bits, but the meaning depends on how a program interprets them. Text (character) data.txt, .csv, .java—is read and written as characters. Binary (byte) data.jpg, .mp3, .pdf, .class—is read and written as raw bytes. Java accordingly has two I/O hierarchies:

Characters must be encoded into bytes for storage (UTF-8, UTF-16, …) and decoded when read; InputStreamReader/OutputStreamWriter bridge the two worlds.

12.2 Writing and Reading Text Files

PrintWriter is a convenient Writer for text output; BufferedReader reads text efficiently, line by line. Both implement AutoCloseable, so a try-with-resources block closes them automatically—even if an exception occurs.

Listing: WriteTextFile.java

// WriteTextFile.java — Writing a text file with PrintWriter (try-with-resources).
import java.io.IOException;
import java.io.PrintWriter;

public class WriteTextFile {
    public static void main(String[] args) throws IOException {
        try (PrintWriter writer = new PrintWriter("output.txt")) {
            writer.println("Hello, Java I/O!");
            writer.println("This is line 2.");
            writer.printf("Pi is approximately %.4f%n", 3.14159);
        }
        System.out.println("Wrote output.txt");
    }
}

Listing: ReadTextFile.java

// ReadTextFile.java — Reading a text file line by line with BufferedReader.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ReadTextFile {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader("output.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

Running WriteTextFile then ReadTextFile:

Wrote output.txt
Hello, Java I/O!
This is line 2.
Pi is approximately 3.1416

The try (…) header declares the resource; it is closed automatically at the end of the block, which is why no explicit close() call is needed. main declares throws IOException because these operations can fail (e.g., the file cannot be created).

12.3 Byte Streams: Copying a Binary File

For binary data you use FileInputStream and FileOutputStream. The pattern below reads each byte (an int from 0–255, or -1 at end of file) and writes it out.

Listing: CopyBinaryFile.java

// CopyBinaryFile.java — Byte-stream copy with FileInputStream/FileOutputStream.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBinaryFile {
    public static void main(String[] args) throws IOException {
        // First, create a small binary source file (bytes 0..255)
        try (FileOutputStream out = new FileOutputStream("source.bin")) {
            for (int i = 0; i < 256; i++) {
                out.write(i);
            }
        }

        // Copy source.bin -> copy.bin one byte at a time
        int total = 0;
        try (FileInputStream in = new FileInputStream("source.bin");
             FileOutputStream out = new FileOutputStream("copy.bin")) {
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
                total++;
            }
        }
        System.out.println("Copied " + total + " bytes from source.bin to copy.bin");
    }
}

Output:

Copied 256 bytes from source.bin to copy.bin

Use byte streams for images, audio, video, PDFs, .class files—anything that is not human-readable text. For better performance, wrap the streams in BufferedInputStream/BufferedOutputStream so data is moved in chunks rather than one byte at a time.

12.4 Data Streams: Primitive Values in Binary

DataOutputStream writes Java primitives in a portable binary format; DataInputStream reads them back. You must read values in the same order and with the same types you wrote them.

Listing: DataStreamDemo.java

// DataStreamDemo.java — Writing and reading primitive values in binary with
// DataOutputStream / DataInputStream.
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataStreamDemo {
    public static void main(String[] args) throws IOException {
        // Write primitives to a binary file
        try (DataOutputStream out =
                 new DataOutputStream(new FileOutputStream("data.bin"))) {
            out.writeInt(100);
            out.writeDouble(3.75);
            out.writeUTF("Hello, Binary I/O!");
        }

        // Read them back in the SAME order they were written
        try (DataInputStream in =
                 new DataInputStream(new FileInputStream("data.bin"))) {
            int i = in.readInt();
            double d = in.readDouble();
            String s = in.readUTF();
            System.out.println("int    = " + i);
            System.out.println("double = " + d);
            System.out.println("string = " + s);
        }
    }
}

Output:

int    = 100
double = 3.75
string = Hello, Binary I/O!

writeInt/readInt, writeDouble/readDouble, and writeUTF/readUTF (UTF-8 strings) are paired: each write method has a matching read method. Mismatching the order or types corrupts the read.

12.5 Object Serialization

Serialization writes the state of an object to a stream; deserialization reconstructs it. Use ObjectOutputStream.writeObject(obj) and ObjectInputStream.readObject(). The object's class must implement java.io.Serializable (a marker interface with no methods). Mark fields transient to exclude them (e.g., passwords), and give the class a serialVersionUID to keep versions compatible. Because readObject returns Object, you cast it back to the real type.

Listing: ObjectSerializationDemo.java

// ObjectSerializationDemo.java — Worked example for Chapter 12.
// Serializes StudentSer objects to a file with ObjectOutputStream, then deserializes
// them with ObjectInputStream. The class must implement Serializable.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectSerializationDemo {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {

        // Write (serialize) two objects to a file
        try (ObjectOutputStream out =
                 new ObjectOutputStream(new FileOutputStream("students.dat"))) {
            out.writeObject(new StudentSer("Alice", 20, 3.85));
            out.writeObject(new StudentSer("Bob", 22, 3.60));
        }

        // Read (deserialize) them back
        try (ObjectInputStream in =
                 new ObjectInputStream(new FileInputStream("students.dat"))) {
            StudentSer s1 = (StudentSer) in.readObject();
            StudentSer s2 = (StudentSer) in.readObject();
            System.out.println(s1);
            System.out.println(s2);
        }
    }
}

class StudentSer implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private double gpa;

    public StudentSer(String name, int age, double gpa) {
        this.name = name;
        this.age = age;
        this.gpa = gpa;
    }

    @Override
    public String toString() {
        return "Student[name=" + name + ", age=" + age + ", gpa=" + gpa + "]";
    }
}

Output:

Student[name=Alice, age=20, gpa=3.85]
Student[name=Bob, age=22, gpa=3.6]

The two StudentSer objects are written to students.dat as a binary stream of their field values and then reconstructed with their data intact. main declares throws IOException, ClassNotFoundException because readObject can throw the latter if the class is missing.

Chapter Summary

Review Questions

  1. What is a stream, and what is the difference between a character stream and a byte stream?
  2. Why does text I/O involve encoding and decoding, while binary I/O does not?
  3. What does try-with-resources do, and why is it preferable to manual close() calls?
  4. Which classes would you use to (a) write a text file and (b) copy an image file?
  5. In DataStreamDemo, what would go wrong if you called readDouble before readInt?
  6. What must a class implement to be serializable? Is Serializable a marker interface?
  7. What is serialVersionUID for, and what does the transient keyword do?
  8. Why does readObject() return Object, and why must you cast the result?
  9. Which two exceptions does ObjectSerializationDemo.main declare, and why?
  10. Give one situation where you would choose a byte stream over a character stream.

Programming Exercises

  1. Write a program that writes the integers 1–100 to a text file, one per line, then reads them back and prints their sum.
  2. Write a program that appends a line to an existing text file (use new FileWriter(file, true) for append mode).
  3. Write a program that copies source.bin to copy.bin using BufferedInputStream/BufferedOutputStream and reports the time taken.
  4. Write a program that writes an array of double values to a .bin file with DataOutputStream and reads them back.
  5. Make a Book class Serializable (title, author, price) and write an ArrayList<Book> to a file using writeObject; read it back and print each book.
  6. Write a program that counts the number of lines and characters in a text file using BufferedReader.

Part IV — Data Structures, Collections, Lambdas, and Streams

Chapter 13 — Generic Classes and Methods

Generics let you write classes and methods that work with a type parameter so the same code can be reused safely for many types, with errors caught at compile time rather than runtime. Think of a generic type as a label on a box: without a label, anything can go in, but you discover the mistake only when you reach in; with a label, the wrong item is refused up front. This chapter covers generic classes, generic methods, bounded type parameters, and multiple type parameters.

After studying this chapter you will be able to:

13.1 Motivation: The Problem with Raw Types

Before generics, ArrayList held Object, so anything could be added—but retrieving an item required a cast, and a wrong cast blew up only at runtime:

ArrayList list = new ArrayList();   // raw — a "plain box"
list.add("Hello");
list.add(42);                      // anything goes
String s = (String) list.get(1);   // CRASH at runtime: 42 is not a String

With generics you label the box, and the compiler refuses the wrong type:

ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // compile-time error: 42 is not a String
String s = list.get(0);           // no cast needed

The error moves from runtime to compile time, which is the whole point of generics.

13.2 Generic Classes

A generic class has a type parameter in its declaration, e.g. class Box<T>. The placeholder T is used inside the class as if it were a real type; the caller supplies the actual type (Box<String>, Box<Integer>). The diamond <> lets the compiler infer the type on the right side from the left.

Listing: GenericBoxDemo.java

// GenericBoxDemo.java — A generic class Box<T> can hold any one type, checked at compile time.
public class GenericBoxDemo {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.set("Hello Generics");
        String s = stringBox.get();          // no cast needed
        System.out.println(s);

        Box<Integer> intBox = new Box<>();
        intBox.set(42);
        int n = intBox.get();               // no cast needed (auto-unbox)
        System.out.println(n);

        // intBox.set("oops"); // compile-time error: wrong type — generics catch this!
    }
}

class Box<T> {
    private T item;
    public void set(T item) { this.item = item; }
    public T get() { return item; }
}

Output:

Hello Generics
42

Box<String> and Box<Integer> are two parameterized uses of one generic class. The compiler ensures a Box<Integer> accepts only Integers, and get() returns Integer directly—no cast, no surprise ClassCastException.

13.3 Generic Methods

A generic method declares its own type parameter, written before the return type. The same method can then be called with arrays of different types, and the compiler infers T from the argument.

Listing: GenericMethodDemo.java

// GenericMethodDemo.java — A generic method prints an array of any type.
public class GenericMethodDemo {
    public static void main(String[] args) {
        Integer[] ints = {1, 2, 3};
        String[] strs = {"a", "b", "c"};
        Double[] dbls = {1.5, 2.5, 3.5};

        printArray(ints);
        printArray(strs);
        printArray(dbls);
    }

    // Generic method: <T> is the type parameter, declared before the return type.
    public static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
}

Output:

1 2 3
a b c
1.5 2.5 3.5

One printArray serves Integer[], String[], and Double[]; without generics you would need three overloaded methods (or one taking Object[] with casts).

13.4 Bounded Type Parameters

Sometimes a type parameter must support certain behavior. A bound restricts T to a subtype of a given type: <T extends Comparable<T>> means T can be any type that is Comparable to itself, so the method may safely call compareTo on T values.

Listing: BoundedTypeDemo.java

// BoundedTypeDemo.java — A bounded type parameter <T extends Comparable<T>>.
public class BoundedTypeDemo {
    public static void main(String[] args) {
        System.out.println("max of ints    = " + max(3, 9, 2));
        System.out.println("max of doubles = " + max(3.5, 9.1, 2.7));
        System.out.println("max of strings  = " + max("pear", "apple", "banana"));
        // max(new Object(), new Object(), new Object()); // error: Object not Comparable
    }

    // Bounded: T must be Comparable<T>, so we can safely call compareTo on the values.
    public static <T extends Comparable<T>> T max(T a, T b, T c) {
        T best = a;
        if (b.compareTo(best) > 0) best = b;
        if (c.compareTo(best) > 0) best = c;
        return best;
    }
}

Output:

max of ints    = 9
max of doubles = 9.1
max of strings  = pear

Because T extends Comparable<T>, the call b.compareTo(best) compiles. Calling max with a non-Comparable type (such as Object) is a compile-time error, not a runtime surprise. (String comparison is lexicographic, so "pear" > "banana".)

13.5 Multiple Type Parameters and Type Erasure

A generic class can have several type parameters. A Pair<K, V> holds a key of type K and a value of type V.

Listing: GenericStackDemo.java

// GenericStackDemo.java — A generic Stack<E> class used with two different element types.
public class GenericStackDemo {
    public static void main(String[] args) {
        Stack<String> words = new Stack<>();
        words.push("Java"); words.push("Generics"); words.push("Stack");
        while (!words.isEmpty()) {
            System.out.print(words.pop() + " ");
        }
        System.out.println();

        Stack<Integer> nums = new Stack<>();
        nums.push(10); nums.push(20); nums.push(30);
        int sum = 0;
        while (!nums.isEmpty()) {
            sum += nums.pop();
        }
        System.out.println("sum = " + sum);
    }
}

class Stack<E> {
    private java.util.ArrayList<E> list = new java.util.ArrayList<>();
    public void push(E e) { list.add(e); }
    public E pop() {
        if (list.isEmpty()) throw new java.util.EmptyStackException();
        return list.remove(list.size() - 1);
    }
    public boolean isEmpty() { return list.isEmpty(); }
}

Output:

Stack Generics Java
sum = 60

One Stack<E> works for both String and Integer. Popping is LIFO, so the words print in reverse order (Stack Generics Java), and 10 + 20 + 30 = 60.

Type erasure. Generics are a compile-time feature: the compiler removes the type parameters (erases them) and inserts the necessary casts, so at runtime Box<String> and Box<Integer> are both just Box. A consequence is that you cannot write new T() or create arrays of parameterized types directly; work with the type parameter through parameters and ArrayList.

Worked Example: A Generic Pair<K, V>

This example uses two type parameters to model a key–value pair, then updates the value.

Listing: GenericPairDemo.java

// GenericPairDemo.java — Worked example for Chapter 13.
// A generic class with TWO type parameters: Pair<K, V>.
public class GenericPairDemo {
    public static void main(String[] args) {
        Pair<String, Integer> p1 = new Pair<>("Alice", 20);
        Pair<String, Double> p2 = new Pair<>("GPA", 3.85);

        System.out.println(p1.getKey() + " -> " + p1.getValue());
        System.out.println(p2.getKey() + " -> " + p2.getValue());

        p1.setValue(21);
        System.out.println("After update: " + p1);
    }
}

class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) { this.key = key; this.value = value; }
    public K getKey() { return key; }
    public V getValue() { return value; }
    public void setValue(V value) { this.value = value; }
    @Override
    public String toString() { return "(" + key + ", " + value + ")"; }
}

Output:

Alice -> 20
GPA -> 3.85
After update: (Alice, 21)

p1 is a Pair<String, Integer> and p2 is a Pair<String, Double>—the same class, two different type instantiations. setValue on p1 accepts only an Integer (so p1.setValue(21) compiles, but p1.setValue(3.85) would not).

Chapter Summary

Review Questions

  1. Why are generics said to move type errors from runtime to compile time? Give the raw-ArrayList example.
  2. What is the diamond operator <>, and when is it used?
  3. Declare a generic class Box<T> with set and get. Why does get on a Box<String> not need a cast?
  4. Where does a generic method declare its type parameter, and how is T inferred at a call?
  5. What does <T extends Comparable<T>> guarantee about T, and what does it let the method body do?
  6. Why does max(new Object(), …) fail to compile in BoundedTypeDemo?
  7. How many type parameters can a generic class have? Give an example with two.
  8. What is type erasure, and what is one restriction it imposes (such as new T())?
  9. In GenericStackDemo, why do the words print as Stack Generics Java rather than Java Generics Stack?
  10. In GenericPairDemo, why would p1.setValue(3.85) not compile?

Programming Exercises

  1. Write a generic class LinkedList<E> (singly linked) with add and get; test it with String and Integer.
  2. Write a generic method <T> int count(T[] array, T target) that counts occurrences of target (use equals).
  3. Write a generic method <T extends Number> double sum(T[] nums) that sums a numeric array of any Number subtype.
  4. Write a generic class Triple<A, B, C> that holds three values of three (possibly different) types.
  5. Write a generic method <T extends Comparable<T>> T min(T a, T b) and test it on Integer, Double, and String.
  6. Write a generic Cache<K, V> class with put and get backed by a HashMap, and test it.

Chapter 14 — Generic Collections

Java's Collections Framework (java.util) provides ready-made, generic data structures so you rarely need to build your own. The core interfaces are Collection (with sub-interfaces List, Set, Queue) and Map. This chapter tours the most common implementations and shows how to choose among them.

After studying this chapter you will be able to:

14.1 The Collections Framework

The framework is built on interfaces, each with several implementations:

Interface Common implementations Key property
List<E> ArrayList, LinkedList Ordered, indexed, allows duplicates
Set<E> HashSet, TreeSet, LinkedHashSet No duplicates; HashSet unordered, TreeSet sorted
Queue<E> ArrayDeque, LinkedList FIFO (or LIFO if used as a stack)
Map<K,V> HashMap, TreeMap, LinkedHashMap Key → value; unique keys; TreeMap sorted by key

All are generic: you write List<String>, Map<String, Integer>, and so on, so the element types are checked at compile time (Chapter 13).

14.2 Lists

A List is an ordered collection with index-based access; duplicates are allowed. ArrayList is backed by an array (fast random access, slow middle insertion); LinkedList is a doubly linked list (fast ends, slower random access).

Listing: ListDemo.java

// ListDemo.java — ArrayList and LinkedList: ordered, indexed, allows duplicates.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        arrayList.add("Java"); arrayList.add("Python"); arrayList.add("C++");
        arrayList.add(1, "Go");           // insert at index 1
        System.out.println("ArrayList: " + arrayList);
        System.out.println("get(2): " + arrayList.get(2));

        List<String> linkedList = new LinkedList<>(arrayList);
        linkedList.addFirst("Rust");
        System.out.println("LinkedList: " + linkedList);
        linkedList.remove("C++");
        System.out.println("After remove(C++): " + linkedList);

        System.out.print("For-each: ");
        for (String s : linkedList) {
            System.out.print(s + " ");
        }
        System.out.println();
    }
}

Output:

ArrayList: [Java, Go, Python, C++]
get(2): Python
LinkedList: [Rust, Java, Go, Python, C++]
After remove(C++): [Rust, Java, Go, Python]
For-each: Rust Java Go Python

add(1, "Go") inserts at index 1, shifting the rest right. LinkedList can be constructed from another collection and offers addFirst/addLast. The for-each loop works on any Iterable, which all collections are.

14.3 Sets

A Set rejects duplicates (the second add of an existing element is ignored). HashSet gives O(1) average add/contains but an unspecified iteration order; TreeSet keeps elements sorted (by natural ordering or a Comparator) with O(log n) operations.

Listing: SetDemo.java

// SetDemo.java — Sets: no duplicates. HashSet is unordered; TreeSet is sorted.
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetDemo {
    public static void main(String[] args) {
        Set<String> hashSet = new HashSet<>();
        hashSet.add("banana"); hashSet.add("apple"); hashSet.add("banana"); // duplicate ignored
        System.out.println("HashSet (unordered, no dups): " + hashSet);

        Set<String> treeSet = new TreeSet<>(hashSet); // sorted by natural ordering
        System.out.println("TreeSet (sorted): " + treeSet);

        System.out.println("Contains apple? " + treeSet.contains("apple"));
        System.out.println("Size: " + treeSet.size());
    }
}

A typical output (the HashSet line order is unspecified and may differ between runs):

HashSet (unordered, no dups): [banana, apple]
TreeSet (sorted): [apple, banana]
Contains apple? true
Size: 2

The second add("banana") has no effect. TreeSet is constructed from the HashSet to give a sorted view. Use LinkedHashSet if you need insertion order.

14.4 Queues

A Queue is a FIFO structure: offer adds to the back, poll removes from the front, peek looks at the front without removing. ArrayDeque is the usual implementation (it can also act as a stack via push/pop).

Listing: QueueDemo.java

// QueueDemo.java — Queue (FIFO) with ArrayDeque: offer/poll/peek.
import java.util.ArrayDeque;
import java.util.Queue;

public class QueueDemo {
    public static void main(String[] args) {
        Queue<String> queue = new ArrayDeque<>();
        queue.offer("Alice");
        queue.offer("Bob");
        queue.offer("Carol");

        System.out.println("Front (peek): " + queue.peek());
        while (!queue.isEmpty()) {
            System.out.print(queue.poll() + " ");
        }
        System.out.println();
    }
}

Output:

Front (peek): Alice
Alice Bob Carol

The elements come out in the same order they went in—first in, first out.

14.5 Maps

A Map stores key→value pairs with unique keys; put adds or overwrites, get retrieves. HashMap has O(1) average operations with unspecified order; TreeMap keeps keys sorted.

Listing: MapDemo.java

// MapDemo.java — Maps: key -> value, unique keys. HashMap is unordered; TreeMap is sorted by key.
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapDemo {
    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 90); scores.put("Bob", 85); scores.put("Carol", 95);
        scores.put("Alice", 92);   // overwrite the previous value

        System.out.println("Bob's score: " + scores.get("Bob"));
        System.out.println("Alice's score (after overwrite): " + scores.get("Alice"));
        System.out.println("Size: " + scores.size());
        System.out.println("Contains Carol? " + scores.containsKey("Carol"));

        for (Map.Entry<String, Integer> e : scores.entrySet()) {
            System.out.println(e.getKey() + " -> " + e.getValue());
        }

        Map<String, Integer> sorted = new TreeMap<>(scores);
        System.out.println("Sorted by key: " + sorted);
    }
}

A typical output (the HashMap entry order is unspecified):

Bob's score: 85
Alice's score (after overwrite): 92
Size: 3
Contains Carol? true
Bob -> 85
Alice -> 92
Carol -> 95
Sorted by key: {Alice=92, Bob=85, Carol=95}

put("Alice", 92) overwrites the earlier 90. Iterate entries with entrySet() and Map.Entry's getKey/getValue. A TreeMap built from the HashMap prints the entries sorted by key.

14.6 The Collections Utility Class

The Collections class provides static helpers that work on any List:

Worked Example: Tallying Word Counts

This program splits a sentence into words and counts how often each word appears, storing the counts in a Map. It uses getOrDefault for a clean increment, then copies the result into a TreeMap to print the words in sorted order.

Listing: WordFrequencyCounter.java

// WordFrequencyCounter.java — Worked example for Chapter 14.
// Counts how often each word appears in a sentence, using a Map.
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class WordFrequencyCounter {
    public static void main(String[] args) {
        String sentence = "java is fun and java is powerful";
        String[] words = sentence.split(" ");

        Map<String, Integer> counts = new HashMap<>();
        for (String word : words) {
            counts.put(word, counts.getOrDefault(word, 0) + 1);
        }

        // Print sorted by word using a TreeMap
        Map<String, Integer> sorted = new TreeMap<>(counts);
        for (Map.Entry<String, Integer> e : sorted.entrySet()) {
            System.out.println(e.getKey() + ": " + e.getValue());
        }
    }
}

Output:

and: 1
fun: 1
is: 2
java: 2
powerful: 1

counts.getOrDefault(word, 0) + 1 either starts a new word at 1 or increments an existing count. The TreeMap ensures the report is alphabetical. The same pattern—map.put(k, map.getOrDefault(k, 0) + 1)—is the standard idiom for tallying with a map.

Chapter Summary

Review Questions

  1. Name the four core collection interfaces and the one key property of each.
  2. When would you choose ArrayList over LinkedList, and vice versa?
  3. What does a Set do with a duplicate add? How do HashSet and TreeSet differ in ordering?
  4. Why is the iteration order of a HashSet or HashMap called "unspecified"? How do you get a sorted view?
  5. What are offer, poll, and peek for a Queue?
  6. What happens when you put a key that already exists in a Map?
  7. How do you iterate the key–value pairs of a Map?
  8. What does Collections.sort do, and what must the elements implement for it to work?
  9. In WordFrequencyCounter, what does getOrDefault(word, 0) return for a word not yet in the map?
  10. Give one task suited to a List, one to a Set, and one to a Map.

Programming Exercises

  1. Write a program that reads words into a List, then prints them sorted with Collections.sort and reversed with Collections.reverse.
  2. Write a program that stores 10 random integers in a Set and prints how many duplicates were rejected.
  3. Write a Map<Character, Integer> that counts how many times each letter appears in a string.
  4. Write a program that uses an ArrayDeque as a stack (push/pop) to reverse a list of strings.
  5. Write a program that maintains a Map<String, String> phone book and supports lookup, add, and remove.
  6. Write a program that reads a list of Double salaries and prints the average, max, and min using Collections methods.

Chapter 15 — Lambdas and Streams

Lambda expressions give Java a lightweight way to pass behavior—short functions—as arguments. Streams let you describe what to do with a sequence of data (filter, transform, aggregate) in a fluent pipeline rather than how to loop over it. Together they enable concise, declarative code. This chapter introduces functional interfaces, lambda syntax, method references, and the Stream API.

After studying this chapter you will be able to:

15.1 Functional Interfaces and Lambda Syntax

A functional interface has exactly one abstract method (for example, Comparator<T> with compare, or Runnable with run). A lambda expression is a concise way to create an instance of a functional interface: (parameters) -> expression or (parameters) -> { statements; }. The compiler infers the parameter types from the target interface.

Comparator<String> byLength = (s1, s2) -> s1.length() - s2.length();
Runnable task = () -> System.out.println("running");

The first lambda takes two Strings and returns an int (matching Comparator<String>); the second takes nothing and returns void (matching Runnable).

Listing: LambdaDemo.java

// LambdaDemo.java — Lambda expressions implement a functional interface inline.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class LambdaDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Carol", "Dave");

        // Sort by length using a lambda that implements Comparator<String>.
        names.sort((s1, s2) -> s1.length() - s2.length());
        System.out.println("By length: " + names);

        // Sort reverse-alphabetically with a lambda.
        names.sort((s1, s2) -> s2.compareTo(s1));
        System.out.println("Reverse alpha: " + names);

        // A Runnable lambda (no parameters, no return).
        Runnable sayHi = () -> System.out.println("Hi from a lambda!");
        sayHi.run();
    }
}

Output:

By length: [Bob, Dave, Alice, Carol]
Reverse alpha: [Dave, Carol, Bob, Alice]
Hi from a lambda!

The sort-by-length lambda puts the 3-letter and 4-letter names first (Bob, Dave), then the 5-letter names (Alice, Carol, in their original relative order, because List.sort is stable). A zero-parameter lambda still needs empty parentheses: () -> ….

15.2 Method References

A method reference is shorthand for a lambda whose body only calls an existing method. System.out::println means x -> System.out.println(x); String::toUpperCase means s -> s.toUpperCase().

Listing: MethodReferenceDemo.java

// MethodReferenceDemo.java — Method references are shorthand for lambdas that just call one method.
import java.util.Arrays;
import java.util.List;

public class MethodReferenceDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Carol");

        // forEach with a lambda
        names.forEach(name -> System.out.println(name));

        // forEach with a method reference (equivalent to the lambda above)
        names.forEach(System.out::println);

        // Method reference to an instance method (toUpperCase) of each element
        names.stream()
             .map(String::toUpperCase)
             .forEach(System.out::println);
    }
}

Output:

Alice
Bob
Carol
Alice
Bob
Carol
ALICE
BOB
CAROL

The two forEach calls are equivalent—System.out::println is just a shorter form of name -> System.out.println(name). String::toUpperCase maps each string to its uppercase version.

15.3 Stream Pipelines

A stream is a possibly infinite sequence of values that supports pipeline operations. A pipeline has a source (collection.stream()), zero or more intermediate operations (filter, map, sorted, distinct, limit), and a terminal operation (collect, forEach, count, reduce). Intermediate operations are lazy—they run only when a terminal operation is reached—and they return a new stream, so they chain. Streams are not data stores; they do not modify their source.

Listing: StreamBasicsDemo.java

// StreamBasicsDemo.java — A Stream pipeline: filter, map, collect.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamBasicsDemo {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

        // Keep even numbers, square them, collect to a list.
        List<Integer> evenSquares = nums.stream()
            .filter(n -> n % 2 == 0)
            .map(n -> n * n)
            .collect(Collectors.toList());

        System.out.println("Even squares: " + evenSquares);
    }
}

Output:

Even squares: [4, 16, 36, 64]

filter(n -> n % 2 == 0) keeps the evens (2, 4, 6, 8); map(n -> n * n) squares each (4, 16, 36, 64); collect(Collectors.toList()) gathers the result into a List. The original nums list is unchanged.

15.4 Reductions: reduce, count, max, average

A reduction combines the elements into a single value. reduce(identity, accumulator) folds the elements with an accumulator; count returns the number of elements; max and min return an Optional (because an empty stream has no max); numeric streams (mapToInt/mapToDouble) offer sum, average, max directly.

Listing: StreamReduceDemo.java

// StreamReduceDemo.java — Reductions: sum, count, max, average.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class StreamReduceDemo {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);

        // Sum with reduce
        int sum = nums.stream().reduce(0, Integer::sum);
        System.out.println("Sum = " + sum);

        // Count and max
        long count = nums.stream().count();
        Optional<Integer> max = nums.stream().max(Integer::compareTo);
        System.out.println("Count = " + count);
        System.out.println("Max = " + max.orElse(-1));

        // Average via mapToInt
        double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0);
        System.out.printf("Average = %.2f%n", avg);
    }
}

Output:

Sum = 31
Count = 8
Max = 9
Average = 3.88

reduce(0, Integer::sum) starts at 0 and adds each element. max returns Optional<Integer>; orElse(-1) unwraps it (giving -1 only if the stream were empty). mapToInt converts to an IntStream whose average returns an OptionalDouble.

Worked Example: Filtering and Sorting Records

This pipeline filters a list of Student records to those with GPA ≥ 3.5, sorts them by GPA descending, uppercases the names, and collects the result; it also computes the average GPA. A record (Java 16+) is a concise, immutable data carrier with auto-generated accessors (s.name(), s.gpa()).

Listing: StreamPipelineDemo.java

// StreamPipelineDemo.java — Worked example for Chapter 15.
// A pipeline that filters, sorts, and transforms a list of records.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamPipelineDemo {
    record Student(String name, double gpa) {}

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Alice", 3.85),
            new Student("Bob", 3.20),
            new Student("Carol", 3.95),
            new Student("Dave", 2.90)
        );

        // Students with GPA >= 3.5, sorted by GPA descending, names in uppercase.
        List<String> topStudents = students.stream()
            .filter(s -> s.gpa() >= 3.5)
            .sorted((a, b) -> Double.compare(b.gpa(), a.gpa()))
            .map(s -> s.name().toUpperCase())
            .collect(Collectors.toList());

        System.out.println("Top students: " + topStudents);

        double avgGpa = students.stream()
            .mapToDouble(Student::gpa)
            .average()
            .orElse(0);
        System.out.printf("Average GPA = %.2f%n", avgGpa);
    }
}

Output:

Top students: [CAROL, ALICE]
Average GPA = 3.48

Only Carol (3.95) and Alice (3.85) clear the 3.5 filter; sorted descending they appear as CAROL, ALICE. The average over all four students is (3.85 + 3.20 + 3.95 + 2.90) / 4 = 3.48. The whole computation is declarative—there are no explicit loops or index variables.

Chapter Summary

Review Questions

  1. What is a functional interface, and how does it relate to a lambda expression?
  2. Write a lambda for Comparator<Integer> that sorts in descending order.
  3. Why does a zero-parameter lambda still need empty parentheses?
  4. Rewrite name -> System.out.println(name) as a method reference.
  5. What is the difference between an intermediate and a terminal stream operation? Name two of each.
  6. Why are intermediate operations called "lazy"?
  7. What does collect(Collectors.toList()) do, and what type does it return?
  8. Why does max return an Optional rather than an int? How do you unwrap it?
  9. In StreamPipelineDemo, what would change in the output if sorted were removed?
  10. What is a record, and how do you access its fields?

Programming Exercises

  1. Write a program that uses a stream to print only the odd numbers from a List<Integer>.
  2. Write a program that takes a list of strings and prints the lengths of those with more than 3 characters, using filter and map.
  3. Write a program that sorts a list of strings by length using a lambda comparator and prints the result.
  4. Write a stream pipeline that produces the product (not sum) of a list of integers using reduce.
  5. Write a program with a record Point(int x, int y); use a stream to find the point with the largest x and print it.
  6. Write a program that reads a sentence, splits it into words, and uses a stream to print the distinct words sorted alphabetically.

Chapter 16 — Recursion

Recursion is a technique in which a method calls itself to solve a smaller version of the same problem. A recursive method needs a base case (which stops the recursion) and a recursive case (which reduces the problem toward the base case). Recursion is often the most natural way to express problems that have a self-similar structure—factorials, Fibonacci numbers, greatest common divisor, and the Tower of Hanoi.

After studying this chapter you will be able to:

16.1 Base Case and Recursive Case

Every recursive method has two parts:

If the recursive case does not move toward a base case, the recursion never stops—each call adds a frame to the call stack until the stack overflows and Java throws StackOverflowError.

16.2 Factorial

The factorial of n is n! = n × (n−1) × … × 1, with the base case 0! = 1. This definition is naturally recursive: n! = n × (n−1)!.

Listing: FactorialDemo.java

// FactorialDemo.java — Recursive factorial with a base case.
public class FactorialDemo {
    public static void main(String[] args) {
        for (int n = 0; n <= 10; n++) {
            System.out.println(n + "! = " + factorial(n));
        }
    }

    public static long factorial(int n) {
        if (n == 0) return 1;          // base case
        return n * factorial(n - 1);    // recursive case
    }
}

Output:

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800

Calling factorial(4) unfolds as 4 * factorial(3)4 * 3 * factorial(2) → … → 4 * 3 * 2 * 1 * factorial(0), and factorial(0) returns 1 (the base case), so the multiplications collapse to 24. Each pending multiplication is a frame on the call stack.

16.3 Fibonacci Numbers

The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, …, defined by fib(0) = 0, fib(1) = 1, and fib(n) = fib(n−1) + fib(n−2) for n > 1. The recursive definition mirrors the mathematical definition directly.

Listing: FibonacciDemo.java

// FibonacciDemo.java — Recursive Fibonacci (fib(0)=0, fib(1)=1, fib(n)=fib(n-1)+fib(n-2)).
public class FibonacciDemo {
    public static void main(String[] args) {
        for (int i = 0; i <= 10; i++) {
            System.out.print(fib(i) + " ");
        }
        System.out.println();
    }

    public static long fib(int n) {
        if (n <= 1) return n;           // base cases
        return fib(n - 1) + fib(n - 2);  // recursive case
    }
}

Output:

0 1 1 2 3 5 8 13 21 34 55

A word of caution: this naive recursion recomputes the same values many times (fib(5) calls fib(4) and fib(3), but fib(4) also calls fib(3)), so it runs in exponential time. For large n, prefer an iterative loop or memoized recursion. The example stays with small n so the cost is negligible.

16.4 Greatest Common Divisor

Euclid's algorithm: gcd(m, n) = gcd(n, m % n), with base case gcd(m, n) = n when m % n == 0.

Listing: GcdDemo.java

// GcdDemo.java — Recursive Euclid's algorithm for the greatest common divisor.
public class GcdDemo {
    public static void main(String[] args) {
        System.out.println("gcd(48, 18) = " + gcd(48, 18));
        System.out.println("gcd(100, 75) = " + gcd(100, 75));
    }

    public static int gcd(int m, int n) {
        if (m % n == 0) return n;        // base case
        return gcd(n, m % n);            // recursive case
    }
}

Output:

gcd(48, 18) = 6
gcd(100, 75) = 25

gcd(48, 18): 48 % 18 = 12 (not 0) → gcd(18, 12)18 % 12 = 6gcd(12, 6)12 % 6 = 0 → return 6. Each recursive call shrinks the second argument, guaranteeing termination.

16.5 Sum of Digits

To sum the digits of n, take the last digit (n % 10) and add the sum of the rest (n / 10). The base case is n == 0 (no digits left).

Listing: SumDigitsDemo.java

// SumDigitsDemo.java — Recursively sum the digits of a non-negative integer.
public class SumDigitsDemo {
    public static void main(String[] args) {
        System.out.println("sumOfDigits(1234) = " + sumOfDigits(1234));
        System.out.println("sumOfDigits(97531) = " + sumOfDigits(97531));
    }

    public static int sumOfDigits(long n) {
        if (n == 0) return 0;                       // base case
        return (int) (n % 10) + sumOfDigits(n / 10); // last digit + rest
    }
}

Output:

sumOfDigits(1234) = 10
sumOfDigits(97531) = 25

sumOfDigits(1234) = 4 + sumOfDigits(123) = 4 + 3 + 2 + 1 + 0 = 10. The argument n / 10 is strictly smaller, so the recursion converges to 0.

16.6 Recursion vs. Iteration

Any recursive method can be rewritten as a loop, and vice versa. Iteration usually uses less memory (no call-stack frames) and is often faster; recursion can be clearer when the problem is naturally self-similar (trees, fractals, divide-and-conquer). Two pitfalls to avoid:

Choose recursion when it makes the code dramatically clearer and the recursion depth is modest; choose iteration when depth or performance matters.

Worked Example: Tower of Hanoi

The Tower of Hanoi has three pegs and n disks of decreasing size on one peg. The goal is to move all disks to another peg, never placing a larger disk on a smaller one. The recursive insight: to move n disks from A to C using B, first move the top n−1 disks from A to B (using C), then move the single largest disk from A to C, then move the n−1 disks from B to C (using A).

Listing: TowerOfHanoi.java

// TowerOfHanoi.java — Worked example for Chapter 16.
// Move n disks from one peg to another using a spare peg, printing each move.
public class TowerOfHanoi {
    public static void main(String[] args) {
        move(3, 'A', 'C', 'B');   // move 3 disks from A to C using B
    }

    // Move n disks from `from` to `to` using `aux` as a spare peg.
    public static void move(int n, char from, char to, char aux) {
        if (n == 1) {
            System.out.println("Move disk 1 from " + from + " to " + to);
            return;
        }
        move(n - 1, from, aux, to);
        System.out.println("Move disk " + n + " from " + from + " to " + to);
        move(n - 1, aux, to, from);
    }
}

Output:

Move disk 1 from A to C
Move disk 2 from A to B
Move disk 1 from C to B
Move disk 3 from A to C
Move disk 1 from B to A
Move disk 2 from B to C
Move disk 1 from A to C

Three disks take 2³ − 1 = 7 moves. Notice how elegant the recursion is: three lines capture the whole strategy, whereas an iterative solution would need an explicit stack. This is a case where recursion shines.

Chapter Summary

Review Questions

  1. What two parts must every recursive method have, and what does each do?
  2. What happens if a recursive method's recursive case does not move toward the base case?
  3. Trace the calls made by factorial(4) on the call stack, and show how the result is computed.
  4. Why is the naive recursive Fibonacci exponential in time? Give an example of redundant computation.
  5. In GcdDemo, why is the recursion guaranteed to terminate?
  6. Rewrite sumOfDigits iteratively using a while loop. Which form do you prefer and why?
  7. How many moves does the Tower of Hanoi need for n disks? Give the formula.
  8. What is StackOverflowError, and how does it relate to recursion depth?
  9. Give one problem where recursion is clearer than iteration, and one where iteration is clearer.
  10. If you needed fib(100), would you use the naive recursion from this chapter? Why or why not?

Programming Exercises

  1. Write a recursive power(x, n) that computes x raised to n (base case n == 0 returns 1).
  2. Write a recursive reversePrint(int n) that prints the digits of n in reverse order.
  3. Write a recursive countDigits(int n) that returns the number of digits in n.
  4. Write a recursive isPalindrome(String s) that checks whether a string is a palindrome by comparing its first and last characters.
  5. Write a recursive sumArray(int[] a, int n) that returns the sum of the first n elements.
  6. Modify TowerOfHanoi to count and print the total number of moves for n = 4 and n = 5.

Chapter 17 — Searching, Sorting, and Big O

Searching and sorting are the most studied operations in computer science. This chapter implements linear and binary search, three simple quadratic sorts (bubble, selection, insertion), and the divide-and-conquer merge sort, then introduces Big-O as the language for comparing algorithm efficiency.

After studying this chapter you will be able to:

Linear search scans the array from left to right and returns the index of the first match, or -1 if the key is absent. It works on any array (sorted or not) and runs in O(n) time—worst case you examine every element.

Listing: LinearSearchDemo.java

// LinearSearchDemo.java — Linear search scans the array left to right.
public class LinearSearchDemo {
    public static void main(String[] args) {
        int[] a = {3, 1, 4, 1, 5, 9, 2, 6};
        System.out.println("5 found at index " + linearSearch(a, 5));
        System.out.println("99 found at index " + linearSearch(a, 99));
    }

    /** Return the index of key in a, or -1 if not found. */
    public static int linearSearch(int[] a, int key) {
        for (int i = 0; i < a.length; i++) {
            if (a[i] == key) return i;
        }
        return -1;
    }
}

Output:

5 found at index 4
99 found at index -1

Binary search requires a sorted array. It compares the key with the middle element and discards the half that cannot contain the key, halving the search range each step. This gives O(log n) time—each comparison removes half the remaining candidates.

Listing: BinarySearchDemo.java

// BinarySearchDemo.java — Binary search on a sorted array (halves the range each step).
public class BinarySearchDemo {
    public static void main(String[] args) {
        int[] a = {1, 3, 5, 7, 9, 11, 13, 15}; // must be sorted
        System.out.println("7  at index " + binarySearch(a, 7));
        System.out.println("10 at index " + binarySearch(a, 10));
    }

    /** Return the index of key in sorted array a, or -(insertionPoint+1) if not found. */
    public static int binarySearch(int[] a, int key) {
        int low = 0, high = a.length - 1;
        while (low <= high) {
            int mid = (low + high) / 2;
            if (a[mid] == key) return mid;
            else if (a[mid] < key) low = mid + 1;
            else high = mid - 1;
        }
        return -low - 1;
    }
}

Output:

7  at index 3
10 at index -6

7 is found at index 3. 10 is not found; the return value -(low+1) = -6 encodes that 10 would insert at index 5 (between 9 and 11). Applying binary search to an unsorted array gives wrong results—sorting is a prerequisite.

17.3 Bubble Sort

Bubble sort repeatedly steps through the array, swapping each adjacent out-of-order pair. After each pass the largest unsorted element "bubbles" to its final place, so the inner loop shrinks by one each pass. It is O(n²).

Listing: BubbleSortDemo.java

// BubbleSortDemo.java — Bubble sort repeatedly swaps adjacent out-of-order pairs.
public class BubbleSortDemo {
    public static void main(String[] args) {
        int[] a = {5, 3, 8, 1, 9, 2};
        bubbleSort(a);
        for (int v : a) System.out.print(v + " ");
        System.out.println();
    }

    public static void bubbleSort(int[] a) {
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = 0; j < a.length - 1 - i; j++) {
                if (a[j] > a[j + 1]) {
                    int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
                }
            }
        }
    }
}

Output:

1 2 3 5 8 9

17.4 Insertion Sort

Insertion sort grows a sorted prefix one element at a time: take element i, shift the larger elements of the prefix one slot right, and drop key into the gap. It is O(n²) in the worst case but O(n) on nearly-sorted data, which makes it good for small or nearly-sorted inputs.

Listing: InsertionSortDemo.java

// InsertionSortDemo.java — Insertion sort grows a sorted prefix one element at a time.
public class InsertionSortDemo {
    public static void main(String[] args) {
        int[] a = {9, 3, 5, 1, 7};
        insertionSort(a);
        for (int v : a) System.out.print(v + " ");
        System.out.println();
    }

    public static void insertionSort(int[] a) {
        for (int i = 1; i < a.length; i++) {
            int key = a[i];
            int j = i - 1;
            while (j >= 0 && a[j] > key) {
                a[j + 1] = a[j];
                j--;
            }
            a[j + 1] = key;
        }
    }
}

Output:

1 3 5 7 9

(Selection sort, the other common quadratic sort, repeatedly selects the smallest remaining element and swaps it into place; it is always O(n²).)

17.5 Big-O Notation

Big-O describes how an algorithm's running time grows with input size n, ignoring constant factors. It answers "what happens when n gets large?" Common growth rates, from best to worst:

Big-O Name Example in this chapter
O(1) constant array index access
O(log n) logarithmic binary search
O(n) linear linear search
O(n log n) linearithmic merge sort
O(n²) quadratic bubble, insertion, selection sort

An O(n²) sort on 10,000 elements does about 100 million comparisons; an O(n log n) sort does about 130 thousand. For large inputs the difference is enormous, which is why merge sort matters.

Worked Example: Merge Sort

Merge sort is divide-and-conquer: split the array in half, recursively sort each half, then merge the two sorted halves. The base case is an array of length < 2 (already sorted). It runs in O(n log n)—the recursion depth is log n and each level merges O(n) work—much faster than O(n²) for large arrays.

Listing: MergeSortDemo.java

// MergeSortDemo.java — Worked example for Chapter 17.
// Merge sort: divide the array in half, sort each half, then merge the sorted halves.
public class MergeSortDemo {
    public static void main(String[] args) {
        int[] a = {8, 3, 5, 1, 9, 2, 7, 4};
        mergeSort(a);
        for (int v : a) System.out.print(v + " ");
        System.out.println();
    }

    public static void mergeSort(int[] a) {
        if (a.length < 2) return;          // base case
        int mid = a.length / 2;
        int[] left = new int[mid];
        int[] right = new int[a.length - mid];
        for (int i = 0; i < mid; i++) left[i] = a[i];
        for (int i = mid; i < a.length; i++) right[i - mid] = a[i];

        mergeSort(left);
        mergeSort(right);
        merge(a, left, right);
    }

    public static void merge(int[] a, int[] left, int[] right) {
        int i = 0, j = 0, k = 0;
        while (i < left.length && j < right.length) {
            if (left[i] <= right[j]) a[k++] = left[i++];
            else a[k++] = right[j++];
        }
        while (i < left.length) a[k++] = left[i++];
        while (j < right.length) a[k++] = right[j++];
    }
}

Output:

1 2 3 4 5 7 8 9

The merge step walks both sorted halves with two pointers, always copying the smaller front element into the result. Because the halves are already sorted, one pass suffices—this is what makes merge sort O(n log n) instead of O(n²).

Chapter Summary

Review Questions

  1. Why is linear search O(n) but binary search O(log n)? What precondition does binary search need?
  2. In BinarySearchDemo, what does a return value of -6 mean?
  3. Trace bubble sort on {5, 3, 8, 1} for the first two passes.
  4. Why is insertion sort O(n) on nearly-sorted data but O(n²) in the worst case?
  5. What does Big-O measure, and what does it deliberately ignore?
  6. Rank O(1), O(n), O(n²), O(log n), O(n log n) from fastest to slowest growth.
  7. How many comparisons does an O(n²) sort make on 1,000 elements, roughly? How many does an O(n log n) sort make?
  8. Why does merge sort's merge step need only one pass over the two halves?
  9. What is the base case of mergeSort, and why is it needed?
  10. If an array is already sorted, which search would you use, and why?

Programming Exercises

  1. Implement selection sort and compare its output with bubble sort on the same array.
  2. Write a recursive version of binary search (base case: low > high).
  3. Modify BubbleSortDemo to stop early if a pass makes no swaps (an optimized bubble sort).
  4. Write a program that sorts an array with Arrays.sort and times it against bubbleSort on 10,000 random elements.
  5. Write a generic static method <T extends Comparable<T>> void sort(T[] a) that performs insertion sort on any comparable type.
  6. Write a program that demonstrates binary search returning the insertion point for a missing key, and inserts the key there.

Chapter 18 — Custom Generic Data Structures

Java's Collections Framework (Chapter 14) supplies ArrayList, LinkedList, ArrayDeque, and TreeSet, so you rarely need to write your own. Building a few from scratch, however, is the best way to understand how those library structures work and to practice generics (Chapter 13) and recursion (Chapter 16). This chapter implements a generic linked list, stack, queue, and binary search tree.

After studying this chapter you will be able to:

18.1 A Generic Linked List

A linked list stores each element in a separate node that holds the data and a reference to the next node. A head reference points to the first node; the last node's next is null. The list is generic (MyLinkedList<E>), and the Node is a private static nested class so its details do not leak out.

Listing: LinkedListDemo.java

// LinkedListDemo.java — A generic singly linked list built from scratch.
public class LinkedListDemo {
    public static void main(String[] args) {
        MyLinkedList<String> list = new MyLinkedList<>();
        list.add("Alice"); list.add("Bob"); list.add("Carol");
        list.display();
        System.out.println("Size: " + list.size());

        list.add(1, "Mia");   // insert at index 1
        list.display();
    }
}

class MyLinkedList<E> {
    private static class Node<E> {
        E data;
        Node<E> next;
        Node(E data) { this.data = data; }
    }
    private Node<E> head;
    private int size = 0;

    public void add(E e) {
        if (head == null) {
            head = new Node<>(e);
        } else {
            Node<E> p = head;
            while (p.next != null) p = p.next;
            p.next = new Node<>(e);
        }
        size++;
    }

    public void add(int index, E e) {
        if (index == 0) {
            Node<E> n = new Node<>(e);
            n.next = head;
            head = n;
        } else {
            Node<E> p = head;
            for (int i = 0; i < index - 1; i++) p = p.next;
            Node<E> n = new Node<>(e);
            n.next = p.next;
            p.next = n;
        }
        size++;
    }

    public int size() { return size; }

    public void display() {
        StringBuilder sb = new StringBuilder("[");
        Node<E> p = head;
        while (p != null) {
            sb.append(p.data);
            if (p.next != null) sb.append(", ");
            p = p.next;
        }
        sb.append("]");
        System.out.println(sb);
    }
}

Output:

[Alice, Bob, Carol]
Size: 3
[Alice, Mia, Bob, Carol]

add(E) walks to the end and appends; add(1, "Mia") walks to the node before index 1 and splices in the new node. Unlike an array, a linked list inserts in the middle without shifting elements—only two pointers change.

18.2 A Generic Stack

A stack is a LIFO (last-in, first-out) collection: push adds to the top, pop removes from the top, peek reads the top without removing. This wrapper exposes push/pop/peek/isEmpty.

Listing: StackDemo.java

// StackDemo.java — A generic stack (LIFO) built from scratch.
public class StackDemo {
    public static void main(String[] args) {
        MyStack<Integer> stack = new MyStack<>();
        stack.push(10); stack.push(20); stack.push(30);
        System.out.println("Top: " + stack.peek());
        while (!stack.isEmpty()) {
            System.out.print(stack.pop() + " ");
        }
        System.out.println();
    }
}

class MyStack<E> {
    private java.util.ArrayList<E> list = new java.util.ArrayList<>();

    public void push(E e) { list.add(e); }
    public E pop() { return list.remove(list.size() - 1); }
    public E peek() { return list.get(list.size() - 1); }
    public boolean isEmpty() { return list.isEmpty(); }
}

Output:

Top: 30
30 20 10

Popping reverses the push order—30 (last pushed) comes out first, then 20, then 10. Stacks are the natural structure for reversing, for expression evaluation, and for depth-first traversal.

18.3 A Generic Queue

A queue is a FIFO (first-in, first-out) collection: offer adds to the back, poll removes from the front. A LinkedList makes a natural backing store because adding/removing at the ends is O(1).

Listing: QueueDemo.java

// QueueDemo.java — A generic queue (FIFO) built from scratch.
public class QueueDemo {
    public static void main(String[] args) {
        MyQueue<String> q = new MyQueue<>();
        q.offer("Alice"); q.offer("Bob"); q.offer("Carol");
        System.out.println("Front: " + q.peek());
        while (!q.isEmpty()) {
            System.out.print(q.poll() + " ");
        }
        System.out.println();
    }
}

class MyQueue<E> {
    private java.util.LinkedList<E> list = new java.util.LinkedList<>();

    public void offer(E e) { list.addLast(e); }
    public E poll() { return list.removeFirst(); }
    public E peek() { return list.getFirst(); }
    public boolean isEmpty() { return list.isEmpty(); }
}

Output:

Front: Alice
Alice Bob Carol

The elements come out in the same order they went in—Alice first. Queues model buffers, breadth-first traversal, and task scheduling.

18.4 When to Build Your Own vs. Use the Framework

Build your own when the goal is learning the structure or when you need behavior the framework does not provide (a bounded stack, a priority queue with a custom comparator, a tree with parent links). For ordinary programs, prefer java.util's ArrayList/LinkedList, ArrayDeque (stack or queue), and TreeSet/TreeMap—they are battle-tested, fast, and integrate with streams and the rest of the framework.

Worked Example: A Generic Binary Search Tree

A binary search tree (BST) keeps values ordered: for each node, everything in the left subtree is smaller and everything in the right subtree is larger. Insertion and traversal are naturally recursive. An in-order traversal (left subtree, node, right subtree) visits the values in sorted order—so a BST doubles as a sorting structure. The tree is generic with a bounded type parameter E extends Comparable<E> so compareTo is available.

Listing: BinarySearchTreeDemo.java

// BinarySearchTreeDemo.java — Worked example for Chapter 18.
// A generic binary search tree: insert Comparable values, then an in-order
// traversal prints them sorted (ties together generics, recursion, and trees).
public class BinarySearchTreeDemo {
    public static void main(String[] args) {
        MyBST<Integer> tree = new MyBST<>();
        int[] vals = {50, 30, 70, 20, 40, 60, 80};
        for (int v : vals) tree.insert(v);

        System.out.print("In-order: ");
        tree.inorder();
        System.out.println();
    }
}

class MyBST<E extends Comparable<E>> {
    private static class Node<E> {
        E data;
        Node<E> left, right;
        Node(E data) { this.data = data; }
    }
    private Node<E> root;

    public void insert(E e) {
        root = insert(root, e);
    }

    private Node<E> insert(Node<E> node, E e) {
        if (node == null) return new Node<>(e);
        int cmp = e.compareTo(node.data);
        if (cmp < 0)      node.left = insert(node.left, e);
        else if (cmp > 0)  node.right = insert(node.right, e);
        // if cmp == 0 the value is a duplicate; we do nothing
        return node;
    }

    public void inorder() { inorder(root); }

    private void inorder(Node<E> node) {
        if (node == null) return;
        inorder(node.left);
        System.out.print(node.data + " ");
        inorder(node.right);
    }
}

Output:

In-order: 20 30 40 50 60 70 80

Inserting 50, 30, 70, 20, 40, 60, 80 builds a tree with 50 at the root, 30 and 70 as its children, and so on. The recursive insert walks left or right by comparing with compareTo until it finds a null slot. The recursive in-order traversal then prints the values in ascending order—20 30 40 50 60 70 80—which is the BST's defining property. The E extends Comparable<E> bound is what lets insert call e.compareTo(node.data).

Chapter Summary

Review Questions

  1. What does each node of a singly linked list store, and what marks the end of the list?
  2. Why is inserting in the middle of a linked list cheaper than inserting in the middle of an array?
  3. Why is Node declared as a private static nested class inside MyLinkedList?
  4. Give the difference between a stack and a queue, including which order each removes in.
  5. What does LIFO mean, and what is a typical use of a stack?
  6. What does FIFO mean, and what is a typical use of a queue?
  7. State the binary-search-tree ordering property for every node.
  8. Why does MyBST require E extends Comparable<E>? What would fail to compile without the bound?
  9. Why does an in-order traversal of a BST print the values in sorted order?
  10. Give one situation where you would use a framework collection and one where you would build your own.

Programming Exercises

  1. Add a remove(int index) method to MyLinkedList and test it.
  2. Add a contains(E e) method to MyLinkedList that returns true if the element is present.
  3. Implement MyStack<E> using your own MyLinkedList<E> instead of ArrayList.
  4. Add a size() method to MyBST (count the nodes recursively) and test it.
  5. Add a search(E e) method to MyBST that returns true if a value is in the tree.
  6. Write a preorder traversal for MyBST (node, left, right) and compare its output with inorder.

Chapter 19 — Concurrency and Multithreading

Multithreading lets a program run several tasks concurrently—separate flows of execution (threads) sharing the same memory. Java has built-in support for threads, which is one of its strengths. This chapter shows how to create threads, pause them, protect shared data with synchronization, and manage many tasks with the executor framework.

After studying this chapter you will be able to:

19.1 Creating Threads

A thread is an independent flow of execution within a program. Java's java.lang.Thread class represents one. The easiest way to create a thread is to pass a Runnable (a functional interface with one method, run) to a Thread constructor and call start()—which launches the new thread and invokes run on it. Calling run() directly would not start a thread; it would just run on the current thread.

Listing: ThreadDemo.java

// ThreadDemo.java — Creating threads with Runnable and starting/joining them.
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task1 = () -> {
            for (int i = 1; i <= 3; i++) System.out.println("Task1: " + i);
        };
        Runnable task2 = () -> {
            for (int i = 1; i <= 3; i++) System.out.println("Task2: " + i);
        };

        Thread t1 = new Thread(task1, "T1");
        Thread t2 = new Thread(task2, "T2");
        t1.start();
        t2.start();

        t1.join();   // wait for t1 to finish
        t2.join();   // wait for t2 to finish
        System.out.println("Both threads finished.");
    }
}

A sample run (the two threads' lines interleave in an order that varies between runs):

Task1: 1
Task2: 1
Task1: 2
Task2: 2
Task1: 3
Task2: 3
Both threads finished.

join() blocks the caller until the thread completes, so the final "Both threads finished." line always appears last, even though the Task1/Task2 lines interleave non-deterministically. This non-determinism is the defining feature of concurrency—the scheduler decides when each thread runs.

19.2 Sleeping and Interrupts

Thread.sleep(ms) pauses the current thread for at least the given milliseconds. It can throw InterruptedException if another thread calls interrupt() on it while it sleeps, so the call must be wrapped in a try-catch.

Listing: ThreadSleepDemo.java

// ThreadSleepDemo.java — A thread that sleeps; Thread.sleep can be interrupted.
public class ThreadSleepDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread sleeper = new Thread(() -> {
            try {
                System.out.println("Sleeper going to sleep");
                Thread.sleep(300);
                System.out.println("Sleeper woke up");
            } catch (InterruptedException e) {
                System.out.println("Sleeper was interrupted");
            }
        });
        sleeper.start();
        sleeper.join();   // wait for the sleeper to finish
        System.out.println("Main done");
    }
}

Output (the order is deterministic here because join makes main wait):

Sleeper going to sleep
Sleeper woke up
Main done

19.3 Thread States

A thread moves through several states: NEW (created, not started), RUNNABLE (started, eligible to run), BLOCKED (waiting to acquire a lock), WAITING / TIMED_WAITING (waiting on another thread or for a timeout), and TERMINATED (its run method has returned). start() moves a thread from NEW to RUNNABLE; sleep puts it in TIMED_WAITING; join on another thread puts the caller in WAITING until that thread terminates.

19.4 Synchronization and Race Conditions

When two threads update the same field simultaneously, their reads and writes can interleave and lose updates—a race condition. Incrementing counter++ is not atomic (it reads, adds, writes), so two threads can both read the old value and both write old+1, losing one increment. A synchronized method allows only one thread at a time to execute it, making the operation atomic.

Listing: SynchronizationDemo.java

// SynchronizationDemo.java — A synchronized method prevents a race condition.
public class SynchronizationDemo {
    private static int counter = 0;

    // `synchronized` makes only one thread run this method at a time.
    public static synchronized void increment() {
        counter++;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable inc = () -> {
            for (int i = 0; i < 10000; i++) increment();
        };
        Thread t1 = new Thread(inc);
        Thread t2 = new Thread(inc);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Counter = " + counter); // always 20000 with synchronized
    }
}

Output:

Counter = 20000

With the synchronized keyword, the two threads cannot run increment at the same time, so all 20,000 increments are counted. Without synchronized, the result could be less than 20,000 on some runs—lost updates. Synchronization fixes the race but adds contention, so synchronize only what you must.

19.5 The Executor Framework

Managing Thread objects by hand is tedious and error-prone. The executor framework (java.util.concurrent) lets you submit Runnable/Callable tasks to a pool and let the library handle scheduling. Executors.newFixedThreadPool(n) creates a pool of n worker threads; submit queues a task; shutdown stops accepting new tasks; awaitTermination waits for queued tasks to finish.

Listing: ExecutorDemo.java

// ExecutorDemo.java — Submitting tasks to a thread pool with an ExecutorService.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        for (int i = 1; i <= 3; i++) {
            int taskId = i;
            pool.submit(() -> System.out.println(
                "Task " + taskId + " on " + Thread.currentThread().getName()));
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("All tasks done");
    }
}

A sample run (which worker runs which task varies):

Task 1 on pool-1-thread-1
Task 2 on pool-1-thread-2
Task 3 on pool-1-thread-1
All tasks done

Three tasks were served by two pool threads; the library reused them instead of creating a new thread per task. The final line is deterministic because awaitTermination waits for all tasks.

Worked Example: Parallel Sum with Callable and Future

Callable<V> is like Runnable but returns a value of type V; submit(callable) returns a Future<V> whose get() blocks until the result is ready. This program splits an array into two halves, sums each half in a separate pool thread, and combines the two Future<Long> results.

Listing: ParallelSumDemo.java

// ParallelSumDemo.java — Worked example for Chapter 19.
// Sums an array in parallel by submitting two Callable tasks to a thread pool
// and combining their Future results.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ParallelSumDemo {
    public static void main(String[] args) throws Exception {
        int[] data = new int[1000];
        for (int i = 0; i < data.length; i++) data[i] = i + 1; // values 1..1000

        ExecutorService pool = Executors.newFixedThreadPool(2);
        Callable<Long> leftHalf  = () -> sum(data, 0, 500);
        Callable<Long> rightHalf = () -> sum(data, 500, 1000);

        Future<Long> f1 = pool.submit(leftHalf);
        Future<Long> f2 = pool.submit(rightHalf);
        long total = f1.get() + f2.get();   // blocks until each result is ready

        pool.shutdown();
        System.out.println("Parallel sum 1..1000 = " + total); // 500500
    }

    static long sum(int[] a, int from, int to) {
        long s = 0;
        for (int i = from; i < to; i++) s += a[i];
        return s;
    }
}

Output:

Parallel sum 1..1000 = 500500

The two halves are summed concurrently; f1.get() and f2.get() block until each is done, then their results are added. The answer 500500 (the sum of 1…1000) is deterministic even though the order in which the two tasks complete is not.

Chapter Summary

Review Questions

  1. What is the difference between calling t.start() and calling t.run() directly?
  2. Why does the output of ThreadDemo interleave differently on different runs?
  3. What does join() do, and how does it make the final line of ThreadDemo deterministic?
  4. Why must Thread.sleep be wrapped in a try-catch?
  5. What is a race condition, and what can go wrong with an unsynchronized counter++?
  6. What does the synchronized keyword guarantee about a method?
  7. Name the six thread states in order.
  8. Why is an ExecutorService preferable to creating Thread objects by hand?
  9. What is the difference between Runnable and Callable?
  10. In ParallelSumDemo, what does f1.get() do, and is the final total deterministic?

Programming Exercises

  1. Modify ThreadDemo to start three threads that each print their name five times, then join all three.
  2. Write a program that starts a thread which loops 1–5 printing each number with a 100ms sleep between prints.
  3. Remove the synchronized keyword from SynchronizationDemo and run it many times; observe results below 20000.
  4. Write a program that uses Executors.newFixedThreadPool(4) to run five Runnable tasks that each print a message, then shuts down.
  5. Write a Callable<Integer> that returns the factorial of a number; submit it and print the result via Future.
  6. Write a program that sums four quarters of a 4,000-element array in parallel using four Callable<Long> tasks and combines the results.