Java Ecosystem & Programming
- A dedicated section focusing on the Java programming language, including Virtual Machine (JVM) internals, core syntax, and enterprise framework integration.
- 1º Java Foundations
- 2º Java Book - OOP Foundations
- 3º Java - The Java Engineer: Data, Testing & Tooling:
1º Java Foundations
Book 1: Java Foundations
Chapter 1: The Hello World.
1. Brief Theory: Your First Program!
Every journey begins with a first step, and in programming, that step is almost always "Hello World!". This simple program introduces you to the fundamental structure of a Java application.
- What is a Class? Think of a class as a blueprint or a template for creating objects. For now, just understand that every piece of Java code lives inside a class. It's like a container for your program's instructions.
- The
mainMethod: The Program's Entry Point. When you run a Java application, the Java Virtual Machine (JVM) looks for a special method calledmain. This is where your program starts executing. It's the "front door" of your application.public: This keyword means themainmethod can be accessed from anywhere. The JVM needs to access it.static: This means themainmethod belongs to the class itself, not to an object of the class. You don't need to create an object of your class to runmain.void: This means themainmethod doesn't return any value after it finishes its job.main: The actual name of the method.(String[] args): This is an array ofStringobjects that can hold command-line arguments. You won't use this much initially, but it's there if you need to pass information to your program when you run it.
System.out.println()vs.System.out.printf(): Showing Output.System.out.println(): This is your go-to command for printing text to the console (your screen). Thelnat the end stands for "line," meaning it prints your text and then moves to the next line.System.out.printf(): This stands for "print formatted." It's more powerful when you need to display text and variables with specific formatting (like showing only two decimal places for a number). It doesn't automatically move to the next line after printing, so you usually need to add\nfor a newline.
2. Professional Code: Hello World Examples
Example 1.1: Classic Hello World with println
// Chapter1/HelloWorldClassic.java
public class HelloWorldClassic {
public static void main(String[] args) {
System.out.println("Hello, Java World!");
System.out.println("My first program is running!");
}
}
Example 1.2: Using printf for a Simple Message
// Chapter1/HelloWorldPrintf.java
public class HelloWorldPrintf {
public static void main(String[] args) {
String greeting = "Welcome";
String language = "Java";
System.out.printf("%s to %s Programming!\n", greeting, language);
System.out.printf("This is cool, right?");
}
}
Example 1.3: Combining println and printf
// Chapter1/CombinedOutput.java
public class CombinedOutput {
public static void main(String[] args) {
System.out.println("--- Program Start ---");
String userName = "DAM Student";
int year = 2024;
System.out.printf("Hello, %s! It's currently the year %d.\n", userName, year);
System.out.println("Let's learn some awesome Java!");
System.out.println("--- Program End ---");
}
}
3. Line-by-Line Breakdown
Let's break down HelloWorldClassic.java:
// Chapter1/HelloWorldClassic.java
// This is a single-line comment. It's ignored by the compiler but helps humans understand the code.
public class HelloWorldClassic {
// 'public' makes this class accessible from anywhere.
// 'class' keyword declares a new class.
// 'HelloWorldClassic' is the name of our class. By convention, class names start with an uppercase letter.
public static void main(String[] args) {
// 'public': The JVM can access this method.
// 'static': The method belongs to the class itself, not an instance.
// 'void': This method doesn't return any value.
// 'main': The special name the JVM looks for as the starting point.
// '(String[] args)': Allows command-line arguments (we won't use them now).
// The curly braces `{}` define the block of code that belongs to the main method.
System.out.println("Hello, Java World!");
// 'System': A built-in Java class that provides access to system resources.
// '.out': A static member of the System class, representing the standard output stream (usually your console).
// '.println()': A method of the PrintStream object (out) that prints the given argument to the console
// and then moves the cursor to the next line.
// "Hello, Java World!": This is a String literal, the actual text to be printed.
System.out.println("My first program is running!");
// Another call to println(), printing a different message on a new line.
}
}
Now for HelloWorldPrintf.java:
public class HelloWorldPrintf {
public static void main(String[] args) {
String greeting = "Welcome";
// Declares a variable named 'greeting' of type String and assigns it the value "Welcome".
// We'll dive deep into variables in the next chapter!
String language = "Java";
// Declares another String variable named 'language' and assigns "Java".
System.out.printf("%s to %s Programming!\n", greeting, language);
// 'printf()': Prints formatted output.
// "%s": These are format specifiers. %s is a placeholder for a String.
// The values after the comma (greeting, language) are inserted into the placeholders in order.
// '\n': This is an "escape sequence" for a newline character. It explicitly tells printf to move to the next line.
System.out.printf("This is cool, right?");
// Another printf() call. Notice there's no '\n' here, so the next output would appear on the same line
// if we had more print statements afterwards.
}
}
4. Clean Code Pro-Tips
- Class Names: Always use
CamelCasefor class names (e.g.,MyAwesomeProgram,HelloWorld). Start with an uppercase letter. - File Names: Your Java source file (
.javafile) must have the exact same name as your public class, including capitalization (e.g.,HelloWorldClassic.javaforpublic class HelloWorldClassic). - Indentation: Use consistent indentation (4 spaces or 1 tab) to make your code readable. Most IDEs (Integrated Development Environments like IntelliJ IDEA or VS Code) will handle this for you automatically.
- Comments: Use comments (
//for single line,/* ... */for multi-line) to explain why you did something, not just what you did. - Meaningful Names: Even for simple examples, use names that describe their purpose (
greeting,languageare better thang,l).
5. Unsolved Exercise: My Introduction
Your task is to create a new Java program that introduces yourself.
- Create a class named
MyIntroduction. - Inside its
mainmethod, print your name. - Then, on a new line, print your current school program (e.g., "DAM Student").
- Finally, print a statement saying you are excited to learn Java, using
printfto insert the current year (e.g., 2024).
6. Complete Solution: My Introduction
// Chapter1/MyIntroduction.java
public class MyIntroduction {
public static void main(String[] args) {
System.out.println("Hello! My name is [Your Name Here].");
System.out.println("I am a DAM student.");
int currentYear = 2024;
System.out.printf("I am excited to learn Java in %d!\n", currentYear);
}
}
Chapter 2: Variables & Primitives.
1. Brief Theory: Storing Information
In programming, we constantly need to store and manipulate data. This is where variables come in. Think of a variable as a named container that holds a specific type of data.
- How to Declare Variables: To use a variable, you first need to declare it. This involves specifying its
type(what kind of data it will hold) and itsname. You can optionally give it an initialvalueright away.DataType variableName = initialValue; - What are Primitive Types? (The Stack)
Java has two main categories of data types: primitive types and reference types. For this chapter, we're focusing on primitive types.
- Primitive types store the actual data value directly. They are simple, fundamental data types that are built into Java.
- When you declare a primitive variable, the Java Virtual Machine (JVM) allocates a small chunk of memory on the Stack. The Stack is a fast, organized area of memory primarily used for local variables and method calls. Values on the Stack are managed very efficiently, and their memory is automatically reclaimed when they go out of scope.
- The "Why": Primitive types are efficient. They don't require the overhead of objects, making them faster and consuming less memory for simple values.
- Common Primitive Types:
int: Used for whole numbers (integers) without decimal points. (e.g.,10,-500,0). It has a specific range, roughly from -2 billion to +2 billion.double: Used for floating-point numbers (numbers with decimal points). This is the most common choice for decimal numbers due to its precision. (e.g.,3.14,-0.5,100.0).boolean: Used for logical values, onlytrueorfalse. Essential for making decisions in your code.char: Used for single characters. Enclosed in single quotes (e.g.,'A','z','5','!').
2. Professional Code: Variables in Action
Example 2.1: Declaring and Initializing Different Primitives
// Chapter2/PrimitiveVariables.java
public class PrimitiveVariables {
public static void main(String[] args) {
// Declare and initialize an integer variable for age
int studentAge = 19;
System.out.println("Student Age: " + studentAge);
// Declare and initialize a double variable for GPA (Grade Point Average)
double studentGPA = 3.85;
System.out.println("Student GPA: " + studentGPA);
// Declare and initialize a boolean variable for enrollment status
boolean isEnrolled = true;
System.out.println("Is Student Enrolled? " + isEnrolled);
// Declare and initialize a char variable for a grade
char studentGrade = 'A';
System.out.println("Student Grade: " + studentGrade);
// You can also declare first, then assign
int numberOfCourses; // Declaration
numberOfCourses = 5; // Assignment
System.out.println("Number of Courses: " + numberOfCourses);
}
}
Example 2.2: Basic Operations with Variables
// Chapter2/VariableOperations.java
public class VariableOperations {
public static void main(String[] args) {
int apples = 10;
int oranges = 5;
int totalFruits = apples + oranges; // Addition
System.out.println("Total fruits: " + totalFruits); // Output: 15
double pricePerKg = 2.50;
int weightKg = 3;
double totalPrice = pricePerKg * weightKg; // Multiplication
System.out.println("Total price: " + totalPrice); // Output: 7.5
// Integer division vs. Double division
int totalStudents = 30;
int groupsOf = 7;
int numberOfGroups = totalStudents / groupsOf; // Integer division truncates decimals
System.out.println("Number of groups (int division): " + numberOfGroups); // Output: 4
double totalScore = 95.5;
double maxScore = 100.0;
double percentage = (totalScore / maxScore) * 100; // Division and multiplication
System.out.println("Percentage: " + percentage + "%"); // Output: 95.5%
// Reassigning values
apples = 12; // Update the value of 'apples'
System.out.println("Updated apples: " + apples); // Output: 12
}
}
Example 2.3: Type Interactions and Limitations
// Chapter2/TypeInteractions.java
public class TypeInteractions {
public static void main(String[] args) {
int score = 85;
double percentage = 0.85;
// You can use different types in calculations, Java will often "promote" the smaller type
// 'score' (int) is promoted to double for the multiplication
double finalValue = score * percentage;
System.out.println("Final Value (int * double): " + finalValue); // Output: 72.25
// Implicit type conversion (widening conversion - safe)
// An int can be assigned to a double without loss of information
double scoreAsDouble = score;
System.out.println("Score as double: " + scoreAsDouble); // Output: 85.0
// Explicit type casting (narrowing conversion - potentially loses information)
// You cannot directly assign a double to an int without a cast.
// The decimal part will be truncated.
int roundedScore = (int) finalValue; // Cast 'finalValue' (double) to int
System.out.println("Rounded Score (int cast): " + roundedScore); // Output: 72
// Boolean values can't be directly converted to numbers
boolean isComplete = true;
// int status = isComplete; // This would cause a compile-time error!
System.out.println("Is complete: " + isComplete);
char initial = 'J';
System.out.println("My initial: " + initial);
// Chars are actually stored as numbers (ASCII/Unicode values).
// You can perform arithmetic on them.
char nextChar = (char)(initial + 1); // 'J' is 74, so 74 + 1 = 75 which is 'K'
System.out.println("Next char after 'J': " + nextChar); // Output: K
}
}
3. Line-by-Line Breakdown
Let's look at PrimitiveVariables.java:
public class PrimitiveVariables {
public static void main(String[] args) {
int studentAge = 19;
// 'int': This is the data type. It specifies that 'studentAge' will hold whole numbers.
// 'studentAge': This is the variable name. It's descriptive and follows Java's naming conventions.
// '=': This is the assignment operator. It assigns the value on the right to the variable on the left.
// '19': This is the literal value (an integer) being assigned to 'studentAge'.
// ';': The semicolon terminates the statement. Every statement in Java must end with one.
System.out.println("Student Age: " + studentAge);
// We use the '+' operator here for string concatenation. It joins the string literal
// "Student Age: " with the current value of the 'studentAge' variable (which is 19).
// The result is a single string "Student Age: 19" which is then printed.
double studentGPA = 3.85;
// 'double': Data type for floating-point numbers (numbers with decimals).
// '3.85': A double literal.
boolean isEnrolled = true;
// 'boolean': Data type that can only hold 'true' or 'false'.
// 'true': A boolean literal.
char studentGrade = 'A';
// 'char': Data type for a single character.
// `'A'`: A character literal, enclosed in single quotes.
int numberOfCourses; // Declaration
// Here, we just declare the variable 'numberOfCourses' of type 'int'.
// It doesn't have a value yet (it will have a default value of 0, but it's good practice to assign explicitly).
numberOfCourses = 5; // Assignment
// Now we assign the value '5' to the already declared variable 'numberOfCourses'.
}
}
4. Clean Code Pro-Tips
- CamelCase for Variables: Variable names should start with a lowercase letter, and then capitalize the first letter of each subsequent word (e.g.,
firstName,totalItemsInCart,isStudentEnrolled). - Meaningful Names: Always choose variable names that clearly describe their purpose.
ageis better thana,numberOfStudentsis better thannum. - Initialization: It's good practice to initialize variables when you declare them if you know their initial value. This avoids potential errors.
- Choose the Right Type: Don't use a
doubleif you only need whole numbers (intis more efficient). Don't use anintif you need decimals (double). - Final Variables: If a variable's value should never change after its initial assignment, declare it with the
finalkeyword (e.g.,final double PI = 3.14159;). This is a constant. By convention,finalvariables are named inSCREAMING_SNAKE_CASE.
5. Unsolved Exercise: Student Profile
Your task is to create a Java program that defines a simple profile for a student.
- Create a class named
StudentProfile. - Declare and initialize variables for:
- The student's
age(anint). - The student's
heightin meters (adouble). - Whether the student is
activein sports (aboolean). - The student's
firstInitialof their name (achar).
- The student's
- Print each of these variables to the console, clearly labeled.
- Then, update the student's age (e.g., add 1 year) and print the new age.
6. Complete Solution: Student Profile
// Chapter2/StudentProfile.java
public class StudentProfile {
public static void main(String[] args) {
// 1. Declare and initialize student profile variables
int studentAge = 18; // Student's age in years
double studentHeightMeters = 1.75; // Student's height in meters
boolean isActiveInSports = true; // Is the student active in sports?
char firstInitial = 'A'; // First initial of the student's name
// 2. Print each variable with clear labels
System.out.println("--- Student Profile ---");
System.out.println("Age: " + studentAge + " years");
System.out.println("Height: " + studentHeightMeters + " meters");
System.out.println("Active in Sports: " + isActiveInSports);
System.out.println("First Initial: " + firstInitial);
System.out.println("-----------------------");
// 3. Update the student's age and print the new age
studentAge = studentAge + 1; // Or studentAge++; which we'll see next chapter!
System.out.println("\n--- After One Year ---");
System.out.println("New Age: " + studentAge + " years");
System.out.println("----------------------");
}
}
Chapter 3: Basic Operators.
1. Brief Theory: Performing Actions
Operators are special symbols that tell the Java compiler to perform specific mathematical, relational, or logical operations and produce a result. They are the verbs of your programming language, allowing you to manipulate variables and values.
-
Arithmetic Operators: These are used to perform basic mathematical calculations.
+(Addition): Adds two operands.-(Subtraction): Subtracts the second operand from the first.*(Multiplication): Multiplies two operands./(Division): Divides the first operand by the second. Crucial: If both operands are integers, the result is an integer (decimal part is truncated, not rounded!). If at least one operand is adouble, the result will be adouble.%(Modulo/Remainder): Returns the remainder of the division of the first operand by the second. Very useful for checking even/odd numbers, or wrapping around values.
-
Increment/Decrement Operators: These are shortcuts for adding or subtracting
1from a variable. They only work on numerical variables.++(Increment): Increases the value of a variable by1.--(Decrement): Decreases the value of a variable by1.- Prefix vs. Postfix: This is important!
- Prefix (
++xor--x): The operation (increment/decrement) is performed first, and then the new value is used in the expression. - Postfix (
x++orx--): The current value of the variable is used in the expression first, and then the variable is incremented/decremented.
- Prefix (
-
Operator Precedence: Just like in mathematics, operators have an order of precedence (e.g., multiplication and division happen before addition and subtraction). You can use parentheses
()to explicitly control the order of operations.
2. Professional Code: Operators in Practice
Example 3.1: Arithmetic Operators
// Chapter3/ArithmeticOperations.java
public class ArithmeticOperations {
public static void main(String[] args) {
int num1 = 20;
int num2 = 6;
// Addition
int sum = num1 + num2;
System.out.println("Sum: " + sum); // Output: 26
// Subtraction
int difference = num1 - num2;
System.out.println("Difference: " + difference); // Output: 14
// Multiplication
int product = num1 * num2;
System.out.println("Product: " + product); // Output: 120
// Division (Integer Division vs. Double Division)
int quotientInt = num1 / num2; // Both are int, so result is int (truncates)
System.out.println("Quotient (int): " + quotientInt); // Output: 3 (20 / 6 = 3 with remainder 2)
double quotientDouble = (double) num1 / num2; // Cast one operand to double for float division
System.out.println("Quotient (double): " + quotientDouble); // Output: 3.3333333333333335
// Modulo (Remainder)
int remainder = num1 % num2;
System.out.println("Remainder: " + remainder); // Output: 2
// Combined operations with precedence
int result = num1 + num2 * 2; // num2 * 2 happens first (6*2=12), then num1 + 12 (20+12=32)
System.out.println("Result (num1 + num2 * 2): " + result); // Output: 32
int resultWithParentheses = (num1 + num2) * 2; // Parentheses force addition first (20+6=26), then 26*2=52
System.out.println("Result ((num1 + num2) * 2): " + resultWithParentheses); // Output: 52
}
}
Example 3.2: Increment and Decrement Operators
// Chapter3/IncrementDecrement.java
public class IncrementDecrement {
public static void main(String[] args) {
int counter = 5;
System.out.println("Initial counter: " + counter); // Output: 5
// Postfix Increment: Use current value, then increment
int postIncResult = counter++;
System.out.println("After post-increment (postIncResult): " + postIncResult); // Output: 5 (used current 5, THEN counter became 6)
System.out.println("Counter after postfix: " + counter); // Output: 6
// Prefix Increment: Increment, then use new value
int preIncResult = ++counter;
System.out.println("After pre-increment (preIncResult): " + preIncResult); // Output: 7 (counter became 7, THEN used 7)
System.out.println("Counter after prefix: " + counter); // Output: 7
// Decrement works similarly
int value = 10;
System.out.println("\nInitial value: " + value); // Output: 10
// Postfix Decrement
int postDecResult = value--;
System.out.println("After post-decrement (postDecResult): " + postDecResult); // Output: 10
System.out.println("Value after postfix: " + value); // Output: 9
// Prefix Decrement
int preDecResult = --value;
System.out.println("After pre-decrement (preDecResult): " + preDecResult); // Output: 8
System.out.println("Value after prefix: " + value); // Output: 8
// Simple increment/decrement (when not part of an assignment)
int score = 0;
score++; // score becomes 1
score--; // score becomes 0
System.out.println("\nFinal score after simple increment/decrement: " + score); // Output: 0
}
}
3. Line-by-Line Breakdown
Let's look at key lines from ArithmeticOperations.java and IncrementDecrement.java:
// From ArithmeticOperations.java
int quotientInt = num1 / num2;
// 'num1' is 20, 'num2' is 6.
// Since both 'num1' and 'num2' are 'int', Java performs integer division.
// 20 divided by 6 is 3 with a remainder of 2. The decimal part (.333...) is simply discarded.
// 'quotientInt' will store '3'.
double quotientDouble = (double) num1 / num2;
// '(double) num1': This is a "type cast". It temporarily converts the value of 'num1' (20) into a 'double' (20.0).
// Now, the expression is '20.0 / 6'. Since one operand is a 'double', Java performs floating-point division.
// The result is '3.3333333333333335'.
// 'quotientDouble' will store '3.3333333333333335'.
int remainder = num1 % num2;
// The '%' operator calculates the remainder after division.
// 20 divided by 6 is 3 with a remainder of 2.
// 'remainder' will store '2'.
int result = num1 + num2 * 2;
// Operator Precedence: Multiplication (*) has higher precedence than addition (+).
// First, 'num2 * 2' is calculated: 6 * 2 = 12.
// Then, 'num1 + 12' is calculated: 20 + 12 = 32.
// 'result' will store '32'.
int resultWithParentheses = (num1 + num2) * 2;
// Parentheses '()' explicitly override precedence. The expression inside parentheses is calculated first.
// First, '(num1 + num2)' is calculated: 20 + 6 = 26.
// Then, '26 * 2' is calculated: 52.
// 'resultWithParentheses' will store '52'.
// From IncrementDecrement.java
int postIncResult = counter++;
// 'counter' is currently 5.
// Postfix '++' means:
// 1. Use the *current* value of 'counter' (5) in the assignment to 'postIncResult'. So, 'postIncResult' becomes 5.
// 2. *Then*, increment 'counter' by 1. So, 'counter' becomes 6.
int preIncResult = ++counter;
// 'counter' is currently 6 (from the previous operation).
// Prefix '++' means:
// 1. Increment 'counter' by 1 *first*. So, 'counter' becomes 7.
// 2. *Then*, use the *new* value of 'counter' (7) in the assignment to 'preIncResult'. So, 'preIncResult' becomes 7.
4. Clean Code Pro-Tips
- Parentheses for Clarity: Even if operator precedence would give the correct result, use parentheses
()in complex expressions to make the order of operations explicit and easier to read. - Avoid Over-Complication with
++/--: While++and--are powerful, avoid using them within complex expressions (e.g.,someMethod(++x, y--)). It can make the code hard to understand and debug. Use them on a separate line for clarity (e.g.,x++;thensomeMethod(x, y);). - Watch Out for Integer Division: Be mindful that dividing two integers (
int / int) will always result in an integer, truncating any decimal part. If you need a precise decimal result, cast at least one of the operands to adouble. - Meaningful Variable Names: Keep using descriptive names for variables that store the results of operations (e.g.,
sum,product,remainder).
5. Unsolved Exercise: Budget Calculator
Imagine you're tracking a small budget.
- Create a class named
BudgetCalculator. - Declare an
intvariable forinitialBudgetand set it to1000. - Declare a
doublevariable foritemPriceand set it to25.50. - Declare an
intvariable forquantityand set it to3. - Calculate the
costOfItems(price * quantity) and store it in adoublevariable. - Calculate the
remainingBudget(initial budget - cost of items) and store it in adoublevariable. - Increment a counter variable
transactionCount(initialized to0) using++after each calculation. - Print the
initialBudget,itemPrice,quantity,costOfItems,remainingBudget, andtransactionCountwith clear labels. - Calculate and print how many times the
itemPrice(as aninttype) could be fully bought with theremainingBudget(also as aninttype - requiring casts). Use the/operator for this.
6. Complete Solution: Budget Calculator
// Chapter3/BudgetCalculator.java
public class BudgetCalculator {
public static void main(String[] args) {
// 1. Declare and initialize variables
int initialBudget = 1000;
double itemPrice = 25.50;
int quantity = 3;
int transactionCount = 0; // Initialize transaction counter
System.out.println("--- Budget Details ---");
System.out.println("Initial Budget: $" + initialBudget);
System.out.println("Item Price: $" + itemPrice);
System.out.println("Quantity to buy: " + quantity);
// 5. Calculate cost of items
double costOfItems = itemPrice * quantity;
System.out.println("Cost of " + quantity + " items: $" + costOfItems);
transactionCount++; // Increment after calculation 1
// 6. Calculate remaining budget
double remainingBudget = initialBudget - costOfItems; // int - double results in double
System.out.println("Remaining Budget: $" + remainingBudget);
transactionCount++; // Increment after calculation 2
// 7. Print transaction count
System.out.println("Total transactions processed: " + transactionCount);
// 9. Calculate how many more items can be bought with remaining budget
// We need to cast remainingBudget and itemPrice to int for integer division,
// which means we're only considering whole items.
int itemsPossibleWithRemainingBudget = (int)remainingBudget / (int)itemPrice;
System.out.println("\nWith remaining budget, " + itemsPossibleWithRemainingBudget + " more full items can be bought.");
}
}
Chapter 4: Strings for Beginners.
1. Brief Theory: Working with Text
So far, we've dealt with numbers, single characters, and true/false values. But what about sequences of characters, like names, sentences, or paragraphs? That's where Strings come in!
- Strings are Objects (The Heap): Unlike
int,double,boolean, andchar(which are primitive types),Stringis a reference type. This means aStringvariable doesn't hold the actual text value itself, but rather a reference (a memory address) to where the text data is stored in memory.- This actual text data is stored on the Heap. The Heap is a larger, more flexible area of memory where objects live. Memory on the Heap is managed by Java's Garbage Collector.
- The "Why": Strings can be of variable length, and Java needs a more dynamic way to manage their memory than the fixed-size allocations on the Stack. Objects on the Heap allow this flexibility.
- String Literals vs.
new String():- String Literal: When you create a string like
String name = "Alice";, Java uses a special area called the "String Pool" (a part of the Heap). If "Alice" already exists in the pool, it simply reuses the existing object. If not, it creates a new one. This is generally more efficient. new String(): When you useString name = new String("Alice");, you explicitly force Java to create a new String object on the Heap, even if "Alice" already exists in the String Pool. This is rarely necessary and less efficient.
- String Literal: When you create a string like
.equals()vs.==: Comparing Strings Correctly. This is one of the most common pitfalls for beginners!==: For objects (likeString),==compares their memory addresses. It checks if two variables refer to the exact same object in memory..equals(): This is a method available to all objects. ForStringobjects, it has been overridden to compare the actual content (the sequence of characters) of the strings.- Rule: Always use
.equals()to compare the content of two strings.
- Common String Methods: Strings come with a rich set of methods (functions that objects can perform) to manipulate text.
length(): Returns the number of characters in the string.toUpperCase(): Returns a new string with all characters converted to uppercase.toLowerCase(): Returns a new string with all characters converted to lowercase.charAt(int index): Returns the character at the specified index (position). Remember, indexing starts from0!indexOf(String str): Returns the index of the first occurrence of the specified substring.replace(char oldChar, char newChar): Returns a new string with all occurrences ofoldCharreplaced bynewChar.substring(int beginIndex, int endIndex): Returns a new string that is a substring of this string. The substring begins at the specifiedbeginIndexand extends to the character atendIndex - 1.concat(String str)/+: Concatenates (joins) two strings. The+operator is often preferred for readability.
2. Professional Code: Strings in Action
Example 4.1: String Declaration and Basic Methods
// Chapter4/StringBasics.java
public class StringBasics {
public static void main(String[] args) {
// String Literal - preferred way
String courseName = "Java Programming Foundations";
System.out.println("Course Name: " + courseName);
// Get length of the string
int nameLength = courseName.length();
System.out.println("Length of Course Name: " + nameLength); // Output: 28
// Convert to uppercase
String upperCaseName = courseName.toUpperCase();
System.out.println("Uppercase: " + upperCaseName); // Output: JAVA PROGRAMMING FOUNDATIONS
// Convert to lowercase
String lowerCaseName = courseName.toLowerCase();
System.out.println("Lowercase: " + lowerCaseName); // Output: java programming foundations
// Get character at a specific index (0-based)
char firstChar = courseName.charAt(0);
char lastChar = courseName.charAt(courseName.length() - 1);
System.out.println("First Character: " + firstChar); // Output: J
System.out.println("Last Character: " + lastChar); // Output: s
// Find the index of a substring
int progIndex = courseName.indexOf("Programming");
System.out.println("Index of 'Programming': " + progIndex); // Output: 5
// Check if a string contains another string
boolean containsFoundations = courseName.contains("Foundations");
System.out.println("Contains 'Foundations'? " + containsFoundations); // Output: true
}
}
Example 4.2: == vs. .equals()
// Chapter4/StringComparison.java
public class StringComparison {
public static void main(String[] args) {
String s1 = "hello"; // String literal
String s2 = "hello"; // String literal (references the same object in String Pool)
String s3 = new String("hello"); // New String object on the Heap
String s4 = "world"; // Different content
System.out.println("s1: " + s1);
System.out.println("s2: " + s2);
System.out.println("s3: " + s3);
System.out.println("s4: " + s4);
System.out.println("--------------------");
// Comparing s1 and s2 (both literals, same content)
System.out.println("s1 == s2: " + (s1 == s2)); // Output: true (same object in pool)
System.out.println("s1.equals(s2): " + s1.equals(s2)); // Output: true (same content)
System.out.println("--------------------");
// Comparing s1 and s3 (s1 literal, s3 new object, same content)
System.out.println("s1 == s3: " + (s1 == s3)); // Output: false (different objects in memory)
System.out.println("s1.equals(s3): " + s1.equals(s3)); // Output: true (same content)
System.out.println("--------------------");
// Comparing s3 and s3 (same object, same content)
System.out.println("s3 == s3: " + (s3 == s3)); // Output: true
System.out.println("s3.equals(s3): " + s3.equals(s3)); // Output: true
System.out.println("--------------------");
// Comparing s1 and s4 (different content)
System.out.println("s1 == s4: " + (s1 == s4)); // Output: false
System.out.println("s1.equals(s4): " + s1.equals(s4)); // Output: false
}
}
Example 4.3: String Concatenation and Manipulation
// Chapter4/StringManipulation.java
public class StringManipulation {
public static void main(String[] args) {
String firstName = "Alice";
String lastName = "Smith";
// Concatenation using '+' operator (most common and readable)
String fullName = firstName + " " + lastName;
System.out.println("Full Name: " + fullName); // Output: Alice Smith
// Concatenation using .concat() method
String greeting = "Hello ".concat(firstName).concat("!");
System.out.println("Greeting: " + greeting); // Output: Hello Alice!
// Replacing characters
String originalText = "Java is fun!";
String replacedText = originalText.replace('a', '@');
System.out.println("Original: " + originalText); // Output: Java is fun!
System.out.println("Replaced 'a' with '@': " + replacedText); // Output: J@v@ is fun!
// Substring
String message = "Welcome to Java Programming";
String sub1 = message.substring(0, 7); // From index 0 up to (but not including) index 7
String sub2 = message.substring(11); // From index 11 to the end
System.out.println("Substring (0, 7): " + sub1); // Output: Welcome
System.out.println("Substring (11): " + sub2); // Output: Java Programming
// Trimming whitespace
String messyString = " Hello World ";
String trimmedString = messyString.trim();
System.out.println("Messy: '" + messyString + "'");
System.out.println("Trimmed: '" + trimmedString + "'");
}
}
3. Line-by-Line Breakdown
Let's break down key lines from StringComparison.java and StringManipulation.java:
// From StringComparison.java
String s1 = "hello";
String s2 = "hello";
// Both 's1' and 's2' are String literals. Java's String Pool optimization means
// they both point to the *exact same "hello"* object in memory.
String s3 = new String("hello");
// Using 'new String("hello")' explicitly creates a *brand new* String object on the Heap.
// Even though its content is "hello", it is a different object in memory than the one 's1' and 's2' point to.
System.out.println("s1 == s2: " + (s1 == s2));
// s1 and s2 both point to the *same* memory location (the same object in the String Pool).
// So, '==' (memory address comparison) returns 'true'.
System.out.println("s1.equals(s2): " + s1.equals(s2));
// The '.equals()' method compares the *content* of the strings.
// "hello" is equal to "hello". So, this returns 'true'.
System.out.println("s1 == s3: " + (s1 == s3));
// 's1' points to the "hello" in the String Pool.
// 's3' points to a *new* "hello" object created outside the String Pool.
// They are *different objects in different memory locations*. So, '==' returns 'false'.
System.out.println("s1.equals(s3): " + s1.equals(s3));
// The '.equals()' method compares the *content*.
// The content of 's1' ("hello") is the same as the content of 's3' ("hello").
// So, this returns 'true'.
// From StringManipulation.java
String fullName = firstName + " " + lastName;
// The '+' operator acts as a concatenation operator when used with strings.
// It joins the string 'firstName' ("Alice"), the string literal " ", and the string 'lastName' ("Smith")
// into a single new string "Alice Smith".
String replacedText = originalText.replace('a', '@');
// '.replace(char oldChar, char newChar)' is a method of the String class.
// It searches the 'originalText' ("Java is fun!") for every occurrence of the character 'a'.
// It then creates a *new* string where each 'a' is replaced by '@'.
// Note: Strings are immutable. This method doesn't change 'originalText'; it returns a *new* string.
String sub1 = message.substring(0, 7);
// '.substring(int beginIndex, int endIndex)' extracts a portion of the string.
// 'beginIndex' (0) is inclusive: the character at index 0 ('W') is included.
// 'endIndex' (7) is exclusive: the character at index 7 (the space after 'e' in 'Welcome') is *not* included.
// The result is "Welcome".
4. Clean Code Pro-Tips
- Always Use
.equals()for Content Comparison: This is the golden rule for strings. Using==will lead to subtle bugs that are hard to find. - Prefer String Literals: Unless you have a specific reason to create a new
Stringobject (which is rare in introductory programming), always use string literals ("some text") for better performance and memory efficiency through the String Pool. - Strings are Immutable: Once a
Stringobject is created, its content cannot be changed. Methods liketoUpperCase(),replace(),substring(), or concatenation (+) always return a newStringobject with the modified content. The original string remains untouched. This is a fundamental concept. - Use
+for Concatenation: For simple concatenation, the+operator is generally more readable than theconcat()method. For very complex string building in loops, considerStringBuilder(a more advanced topic). - Meaningful String Content: Just like variable names, ensure the text content of your strings is clear and serves its purpose.
5. Unsolved Exercise: Message Processor
You've received a secret message!
- Create a class named
MessageProcessor. - Declare two
Stringvariables:word1with the value "secret" andword2with the value "java". - Declare a third
StringvariablesecretMessageas a literal "The secret code is java". - Compare
word1and "SECRET" using.equals(). What is the result? Why? - Compare
word2and the substring "java" extracted fromsecretMessageusing both==and.equals(). Print both results and explain the difference. - Concatenate
word1,word2(in that order, separated by a space) to form a new stringcombinedWord. Print it. - Print
secretMessagein all uppercase. - Find the index of the word "code" in
secretMessageand print it. - Replace all occurrences of 'e' with 'E' in
secretMessageand print the new message.
6. Complete Solution: Message Processor
// Chapter4/MessageProcessor.java
public class MessageProcessor {
public static void main(String[] args) {
// 1. Declare String variables
String word1 = "secret";
String word2 = "java";
String secretMessage = "The secret code is java"; // Literal
System.out.println("--- Message Processing ---");
System.out.println("word1: " + word1);
System.out.println("word2: " + word2);
System.out.println("secretMessage: " + secretMessage);
System.out.println("--------------------------");
// 4. Compare word1 and "SECRET" using .equals()
boolean equalsCaseSensitive = word1.equals("SECRET");
System.out.println("Does 'word1' equal 'SECRET' (case-sensitive)? " + equalsCaseSensitive);
// Explanation: It's false because .equals() is case-sensitive. 'secret' != 'SECRET'.
// If we wanted to compare case-insensitively, we'd use word1.equalsIgnoreCase("SECRET").
// 5. Compare word2 and substring "java" from secretMessage
String subFromSecretMessage = secretMessage.substring(19); // "java" starts at index 19
System.out.println("\nSubstring 'java' from secretMessage: " + subFromSecretMessage);
boolean equalsOperatorComparison = (word2 == subFromSecretMessage);
System.out.println("word2 == subFromSecretMessage: " + equalsOperatorComparison);
// Explanation: False. 'word2' is a literal from the String Pool.
// 'subFromSecretMessage' is a *new* String object created by the substring() method (it's not from the pool).
// They are different objects in memory.
boolean equalsMethodComparison = word2.equals(subFromSecretMessage);
System.out.println("word2.equals(subFromSecretMessage): " + equalsMethodComparison);
// Explanation: True. The content of both strings is "java".
// 6. Concatenate word1 and word2
String combinedWord = word1 + " " + word2;
System.out.println("\nCombined Word: " + combinedWord); // Output: secret java
// 7. Print secretMessage in all uppercase
System.out.println("Secret Message in Uppercase: " + secretMessage.toUpperCase());
// 8. Find the index of "code"
int indexOfCode = secretMessage.indexOf("code");
System.out.println("Index of 'code': " + indexOfCode); // Output: 11
// 9. Replace 'e' with 'E' in secretMessage
String replacedMessage = secretMessage.replace('e', 'E');
System.out.println("Message with 'e' replaced by 'E': " + replacedMessage);
}
}
Chapter 5: Conditionals (IF/ELSE).
1. Brief Theory: Making Decisions
Life is full of decisions, and so is programming! Conditional statements allow your program to execute different blocks of code based on whether certain conditions are true or false. This is how your programs become dynamic and responsive.
- Boolean Logic: The heart of conditionals is boolean logic. Conditions are expressed as boolean expressions (expressions that evaluate to either
trueorfalse).- Comparison Operators: Used to compare two values, resulting in a boolean.
>(greater than)<(less than)>=(greater than or equal to)<=(less than or equal to)==(equal to - for primitives!)!=(not equal to - for primitives!)
- Logical Operators: Used to combine multiple boolean expressions.
&&(AND): Returnstrueif both operands aretrue.||(OR): Returnstrueif at least one operand istrue.!(NOT): Reverses the boolean value (flipstruetofalse, andfalsetotrue).
- Comparison Operators: Used to compare two values, resulting in a boolean.
ifStatement: The most basic conditional. Executes a block of code only if the specified condition istrue.if (condition) { // Code to execute if condition is true }if-elseStatement: Provides an alternative block of code to execute if theifcondition isfalse.if (condition) { // Code if condition is true } else { // Code if condition is false }if-else if-elseChain: Used when you have multiple conditions to check in a specific order. The firsttruecondition's block is executed, and the rest are skipped. The finalelseis a catch-all if none of the precedingiforelse ifconditions are met.if (condition1) { // Code if condition1 is true } else if (condition2) { // Code if condition1 is false, AND condition2 is true } else if (condition3) { // Code if condition1 and condition2 are false, AND condition3 is true } else { // Code if none of the above conditions are true }- Ternary Operator (
? :): A shorthand for simpleif-elsestatements, often used for assigning a value based on a condition. It's a single expression.
This reads as: "Isresult = (condition) ? valueIfTrue : valueIfFalse;conditiontrue? If yes,resultgetsvalueIfTrue. If no,resultgetsvalueIfFalse."
2. Professional Code: Conditionals in Action
Example 5.1: Simple if and if-else
// Chapter5/SimpleConditionals.java
public class SimpleConditionals {
public static void main(String[] args) {
int score = 75;
int passingScore = 60;
// Simple if statement
if (score > passingScore) {
System.out.println("Congratulations! You passed the exam.");
}
// if-else statement
if (score >= 80) {
System.out.println("You got a good grade!");
} else {
System.out.println("You might want to review the material.");
}
System.out.println("Your score: " + score);
// Example with boolean variable
boolean isLoggedIn = false;
if (isLoggedIn) {
System.out.println("Welcome back, user!");
} else {
System.out.println("Please log in to continue.");
}
}
}
Example 5.2: if-else if-else Chain with Logical Operators
// Chapter5/GradingSystem.java
public class GradingSystem {
public static void main(String[] args) {
int studentScore = 88;
char grade;
if (studentScore >= 90) {
grade = 'A';
System.out.println("Excellent! Grade: " + grade);
} else if (studentScore >= 80) { // studentScore is less than 90 AND greater than or equal to 80
grade = 'B';
System.out.println("Very good! Grade: " + grade);
} else if (studentScore >= 70) { // studentScore is less than 80 AND greater than or equal to 70
grade = 'C';
System.out.println("Good effort! Grade: " + grade);
} else if (studentScore >= 60) { // studentScore is less than 70 AND greater than or equal to 60
grade = 'D';
System.out.println("Pass! Grade: " + grade);
} else { // All other cases (studentScore < 60)
grade = 'F';
System.out.println("Unfortunately, you failed. Grade: " + grade);
}
System.out.println("Final Grade: " + grade);
// Example with logical operators: Eligibility check
int age = 20;
boolean isCitizen = true;
if (age >= 18 && isCitizen) { // Both conditions must be true
System.out.println("You are eligible to vote.");
} else {
System.out.println("You are NOT eligible to vote.");
}
boolean hasLicense = false;
boolean hasVehicle = true;
if (hasLicense || hasVehicle) { // At least one condition must be true
System.out.println("You have either a license or a vehicle (or both).");
} else {
System.out.println("You have neither a license nor a vehicle.");
}
boolean isSunny = true;
if (!isSunny) { // Not sunny means it's not true (so it's false)
System.out.println("It's not sunny today. Might rain!");
} else {
System.out.println("It's sunny today. Enjoy!");
}
}
}
Example 5.3: Ternary Operator
// Chapter5/TernaryOperatorExample.java
public class TernaryOperatorExample {
public static void main(String[] args) {
int temperature = 25;
String weatherStatus = (temperature > 20) ? "Warm" : "Cool";
System.out.println("Weather Status: " + weatherStatus); // Output: Warm
temperature = 15;
weatherStatus = (temperature > 20) ? "Warm" : "Cool";
System.out.println("Weather Status: " + weatherStatus); // Output: Cool
// Another example: check if a number is even or odd
int number = 7;
String parity = (number % 2 == 0) ? "Even" : "Odd";
System.out.println(number + " is " + parity); // Output: 7 is Odd
number = 10;
parity = (number % 2 == 0) ? "Even" : "Odd";
System.out.println(number + " is " + parity); // Output: 10 is Even
// Ternary operator can also be used directly in print statements
System.out.println("Is " + number + " positive? " + (number > 0 ? "Yes" : "No"));
}
}
3. Line-by-Line Breakdown
Let's break down key lines from GradingSystem.java and TernaryOperatorExample.java:
// From GradingSystem.java
if (studentScore >= 90) {
grade = 'A';
System.out.println("Excellent! Grade: " + grade);
} else if (studentScore >= 80) {
// This 'else if' block is only reached if 'studentScore >= 90' was FALSE.
// So, effectively, this condition checks if (studentScore < 90 AND studentScore >= 80).
// This implicit chaining is why the order of 'else if' statements matters!
grade = 'B';
System.out.println("Very good! Grade: " + grade);
}
// ... and so on for other else if blocks.
if (age >= 18 && isCitizen) {
// 'age >= 18': This is a boolean expression (e.g., 20 >= 18 is true).
// 'isCitizen': This is a boolean variable (e.g., true).
// '&&' (Logical AND): The entire condition (age >= 18 && isCitizen) is true ONLY if BOTH
// 'age >= 18' is true AND 'isCitizen' is true.
// If age is 20 (true) and isCitizen is true (true), then true && true is true.
System.out.println("You are eligible to vote.");
}
if (hasLicense || hasVehicle) {
// '||' (Logical OR): The entire condition is true if AT LEAST ONE of the
// 'hasLicense' or 'hasVehicle' expressions is true.
// If hasLicense is false and hasVehicle is true, then false || true is true.
System.out.println("You have either a license or a vehicle (or both).");
}
if (!isSunny) {
// '!' (Logical NOT): Inverts the boolean value.
// If 'isSunny' is true, then '!isSunny' becomes false.
// If 'isSunny' is false, then '!isSunny' becomes true.
// Here, if 'isSunny' is true, the condition '!isSunny' evaluates to false, so this block is skipped.
}
// From TernaryOperatorExample.java
String weatherStatus = (temperature > 20) ? "Warm" : "Cool";
// This is the ternary operator. It's a shorthand for a simple if-else that assigns a value.
// (temperature > 20): This is the boolean condition.
// If true, the value "Warm" is assigned to 'weatherStatus'.
// If false, the value "Cool" is assigned to 'weatherStatus'.
// If temperature is 25, (25 > 20) is true, so "Warm" is assigned.
// If temperature is 15, (15 > 20) is false, so "Cool" is assigned.
4. Clean Code Pro-Tips
- Braces are Mandatory (Almost): Even if an
iforelseblock only contains a single statement, it's a very strong best practice to always use curly braces{}. This prevents subtle bugs if you later add more statements to the block. - Order Matters in
else if: The order of yourelse ifconditions is crucial. Place the most specific or narrow conditions first. For example, check forscore >= 90beforescore >= 80. - Clear Boolean Expressions: Make your conditions easy to read. Use parentheses
()for complex logical expressions to explicitly show the grouping and order of operations. - Avoid Deep Nesting: Too many nested
ifstatements (if { if { if { ... } } }) make code hard to read and understand. Try to flatten your logic usingelse ifor by reversing conditions (if (!condition) { ... } else { ... }). - Ternary for Simplicity: Use the ternary operator for simple, single-line conditional assignments. Avoid using it for complex logic or side effects, as it can reduce readability.
5. Unsolved Exercise: Eligibility Checker
Let's create a program that checks various eligibility criteria.
- Create a class named
EligibilityChecker. - Declare an
intvariablecandidateAgeand set it to22. - Declare a
doublevariablegpaand set it to3.5. - Declare a
booleanvariablehasCriminalRecordand set it tofalse. - Use an
if-else if-elsechain to determine if the candidate is eligible for a scholarship:- If
candidateAgeis less than18, print "Too young for scholarship." - Else if
gpais less than3.0, print "GPA too low for scholarship." - Else if
hasCriminalRecordistrue, print "Ineligible due to criminal record." - Else, print "Candidate is eligible for scholarship!"
- If
- Use a ternary operator to set a
StringvariableadmissionStatus. IfcandidateAgeis greater than or equal to18ANDgpais greater than or equal to2.5,admissionStatusshould be "Admitted", otherwise "Denied". PrintadmissionStatus.
6. Complete Solution: Eligibility Checker
// Chapter5/EligibilityChecker.java
public class EligibilityChecker {
public static void main(String[] args) {
// 2. Declare and initialize variables
int candidateAge = 22;
double gpa = 3.5;
boolean hasCriminalRecord = false;
System.out.println("--- Scholarship Eligibility Check ---");
System.out.println("Candidate Age: " + candidateAge);
System.out.println("GPA: " + gpa);
System.out.println("Has Criminal Record: " + hasCriminalRecord);
System.out.println("------------------------------------");
// 5. Scholarship eligibility check using if-else if-else
if (candidateAge < 18) {
System.out.println("Result: Too young for scholarship.");
} else if (gpa < 3.0) {
System.out.println("Result: GPA too low for scholarship.");
} else if (hasCriminalRecord) { // hasCriminalRecord == true is redundant
System.out.println("Result: Ineligible due to criminal record.");
} else {
System.out.println("Result: Candidate is eligible for scholarship!");
}
System.out.println("\n--- Admission Status Check ---");
// 6. Use ternary operator for admission status
String admissionStatus = (candidateAge >= 18 && gpa >= 2.5) ? "Admitted" : "Denied";
System.out.println("Admission Status: " + admissionStatus);
}
}
Chapter 6: The Modern Switch.
1. Brief Theory: Streamlining Decisions
When you have many else if conditions that all check the same variable against different discrete values, an if-else if-else chain can become long and cumbersome. The switch statement provides a cleaner and more readable alternative for such scenarios.
- Why
switch? It's designed for multi-way branching based on the value of a single variable or expression. It improves readability over a longif-else if-elsechain when dealing with specific, enumerable values (like integers, characters, enums, or Strings starting from Java 7). - The Modern
switchExpression (Java 14+): Java has evolved, and theswitchstatement has become much more powerful and less error-prone with the introduction ofswitch expressions(available since Java 14).- Arrow Syntax (
->): Instead ofcase value: ... break;, you now usecase value -> expression;. This is more concise. - No Fall-through: A major advantage is that the
->syntax automatically handles "breaking." You don't need abreakstatement; only the code associated with the matchedcaseis executed. This eliminates the common "fall-through" bugs that plagued traditionalswitchstatements. - Assigning a Value:
switchcan now be used as an expression (it returns a value) which can be assigned directly to a variable. This is extremely useful for calculating a value based on different cases. - Multiple Labels: You can specify multiple
caselabels for the same block of code (e.g.,case MONDAY, TUESDAY -> ...).
- Arrow Syntax (
- The
defaultCase: This is optional but highly recommended. It acts like theelsein anif-elsechain, providing a fallback block of code to execute if none of thecaselabels match the switch expression's value.
2. Professional Code: Modern Switch Examples
Example 6.1: Day of the Week (Modern Switch Statement)
// Chapter6/DayOfWeekModern.java
public class DayOfWeekModern {
public static void main(String[] args) {
int dayNumber = 3; // 1 for Monday, 7 for Sunday
System.out.println("--- Day of Week Checker ---");
System.out.println("Day Number: " + dayNumber);
switch (dayNumber) {
case 1 -> System.out.println("It's Monday. Time to start the week!");
case 2 -> System.out.println("It's Tuesday. Keep up the good work!");
case 3 -> System.out.println("It's Wednesday. Mid-week already!");
case 4 -> System.out.println("It's Thursday. Almost there!");
case 5 -> System.out.println("It's Friday. Weekend is calling!");
case 6 -> System.out.println("It's Saturday. Enjoy your day off!");
case 7 -> System.out.println("It's Sunday. Relax and recharge.");
default -> System.out.println("Invalid day number. Please use 1-7.");
}
System.out.println("---------------------------");
}
}
Example 6.2: Assigning a Value with Modern Switch Expression
// Chapter6/MonthNameSwitchExpression.java
public class MonthNameSwitchExpression {
public static void main(String[] args) {
int month = 7; // July
String monthName = switch (month) {
case 1 -> "January";
case 2 -> "February";
case 3 -> "March";
case 4 -> "April";
case 5 -> "May";
case 6 -> "June";
case 7 -> "July";
case 8 -> "August";
case 9 -> "September";
case 10 -> "October";
case 11 -> "November";
case 12 -> "December";
default -> "Invalid Month";
}; // Semicolon is required when used as an expression!
System.out.println("Month number " + month + " is: " + monthName); // Output: Month number 7 is: July
month = 13;
monthName = switch (month) {
case 1 -> "January";
case 2 -> "February";
case 3 -> "March";
case 4 -> "April";
case 5 -> "May";
case 6 -> "June";
case 7 -> "July";
case 8 -> "August";
case 9 -> "September";
case 10 -> "October";
case 11 -> "November";
case 12 -> "December";
default -> "Invalid Month";
};
System.out.println("Month number " + month + " is: " + monthName); // Output: Month number 13 is: Invalid Month
}
}
Example 6.3: Multiple Case Labels and Block of Code
// Chapter6/SeasonChecker.java
public class SeasonChecker {
public static void main(String[] args) {
int monthNumber = 11; // November
String season = switch (monthNumber) {
case 12, 1, 2 -> "Winter"; // Multiple labels for the same result
case 3, 4, 5 -> "Spring";
case 6, 7, 8 -> "Summer";
case 9, 10, 11 -> "Autumn (Fall)";
default -> "Unknown";
};
System.out.println("Month " + monthNumber + " is in the " + season + " season."); // Output: Month 11 is in the Autumn (Fall) season.
char grade = 'B';
String gradeDescription = switch (grade) {
case 'A', 'a' -> "Excellent work!"; // Handle both upper and lower case
case 'B', 'b' -> "Good job!";
case 'C', 'c' -> "You passed.";
case 'D', 'd', 'F', 'f' -> { // A block of code for a case
System.out.println("--- Action Needed ---");
// You can have multiple statements here
yield "Needs improvement."; // 'yield' is used to return a value from a block in a switch expression
}
default -> "Invalid Grade";
};
System.out.println("For grade " + grade + ": " + gradeDescription);
}
}
3. Line-by-Line Breakdown
Let's break down key lines from DayOfWeekModern.java and SeasonChecker.java:
// From DayOfWeekModern.java
switch (dayNumber) {
// 'switch (dayNumber)': The value of 'dayNumber' (which is 3 in this example) is evaluated.
// The control flow then jumps to the 'case' label that matches this value.
case 1 -> System.out.println("It's Monday. Time to start the week!");
// 'case 1': This label specifies that if 'dayNumber' is 1, the code to its right should be executed.
// '->': The arrow operator. This indicates that the code on the right is the body of this case.
// Importantly, after this line executes, the 'switch' statement is exited automatically (no fall-through).
case 3 -> System.out.println("It's Wednesday. Mid-week already!");
// In our example, 'dayNumber' is 3, so this case matches.
// The System.out.println() statement is executed.
// After this, the switch statement finishes.
default -> System.out.println("Invalid day number. Please use 1-7.");
// 'default': If none of the 'case' labels match the value of 'dayNumber',
// the code associated with the 'default' label is executed. This acts as a catch-all.
}
// From SeasonChecker.java
String season = switch (monthNumber) {
// Here, the 'switch' is used as an *expression*. This means it computes a value
// that is then assigned to the 'season' variable.
case 12, 1, 2 -> "Winter";
// Multiple 'case' labels separated by commas. If 'monthNumber' is 12, 1, or 2,
// the string literal "Winter" is the value produced by this 'switch' expression.
case 9, 10, 11 -> "Autumn (Fall)";
// In our example, 'monthNumber' is 11, so this case matches.
// The value "Autumn (Fall)" is produced by the switch expression and assigned to 'season'.
case 'D', 'd', 'F', 'f' -> {
// Here, a case contains a *block of code* (enclosed in curly braces {}).
// This is useful if you need to perform multiple operations for a specific case.
System.out.println("--- Action Needed ---"); // This line is executed.
yield "Needs improvement."; // 'yield' is a keyword used in switch *expressions* (when the case has a block)
// to explicitly specify the value that the switch expression should return.
}
}; // Don't forget the semicolon here when using switch as an expression!
4. Clean Code Pro-Tips
- Prefer Modern Switch Expressions: Always use the new
->syntax forswitchstatements and expressions (if your Java version is 14 or higher). It's safer (no fall-through) and more concise. - Use
default: Always include adefaultcase to handle unexpected or unhandled values. This makes your code more robust and prevents logical errors. - Multiple Labels for Common Logic: Group multiple
caselabels together if they share the same outcome (e.g.,case 'A', 'a' -> ...). - Use
yieldwith Blocks: If acasein aswitch expressionrequires multiple statements before returning a value, use a code block ({}) and theyieldkeyword to specify the return value. - Don't Forget the Semicolon: When using
switchas an expression (to assign a value to a variable), remember to put a semicolon;after the closing brace of the switch block.
5. Unsolved Exercise: Simple Command Processor
Imagine you're building a simple command-line tool.
- Create a class named
CommandProcessor. - Declare a
Stringvariablecommandand set its value to "start". - Use a modern
switchstatement (not an expression for this part) to process the command:- If
commandis "start", print "Starting service...". - If
commandis "stop", print "Stopping service...". - If
commandis "restart", print "Restarting service. Please wait...". - For any other command, print "Unknown command: [command]".
- If
- Now, declare an
intvariablestatusCodeand set it to1. - Use a modern
switch expressionto determine aStringvariablestatusMessagebased onstatusCode:- If
statusCodeis0,statusMessageshould be "Success". - If
statusCodeis1or2,statusMessageshould be "Warning". - If
statusCodeis3,statusMessageshould be "Error: Critical". - For any other
statusCode,statusMessageshould be "Error: Unknown Code".
- If
- Print the final
statusMessage.
6. Complete Solution: Simple Command Processor
// Chapter6/CommandProcessor.java
public class CommandProcessor {
public static void main(String[] args) {
// Part 1: Processing commands using a switch statement
String command = "start"; // Try changing this to "stop", "restart", or "status"
System.out.println("--- Command Processing ---");
System.out.println("Received command: " + command);
switch (command) {
case "start" -> System.out.println("Starting service...");
case "stop" -> System.out.println("Stopping service...");
case "restart" -> System.out.println("Restarting service. Please wait...");
default -> System.out.println("Unknown command: " + command);
}
System.out.println("--------------------------");
// Part 2: Determining status message using a switch expression
int statusCode = 1; // Try changing this to 0, 2, 3, or 99
System.out.println("\n--- Status Code Processing ---");
System.out.println("Status Code: " + statusCode);
String statusMessage = switch (statusCode) {
case 0 -> "Success";
case 1, 2 -> "Warning"; // Multiple labels for the same result
case 3 -> "Error: Critical";
default -> "Error: Unknown Code";
}; // Semicolon is required here for switch expressions!
System.out.println("Status Message: " + statusMessage);
System.out.println("------------------------------");
}
}
Chapter 7: Loops (FOR / WHILE).
1. Brief Theory: Repeating Actions
Many programming tasks involve repeating a set of instructions multiple times. Instead of writing the same code over and over, we use loops. Loops allow you to execute a block of code repeatedly until a certain condition is met.
- Why Loops?
- Efficiency: Avoids repetitive code (Don't Repeat Yourself - DRY principle).
- Automation: Automates tasks like processing lists of data, counting, or waiting for specific events.
- The
forLoop:- The
forloop is ideal when you know, or can easily determine, the number of times you want to repeat something. - Structure: It has three main parts, separated by semicolons, within its parentheses:
- Initialization: Executed only once at the beginning of the loop. Usually declares and initializes a loop counter variable (e.g.,
int i = 0;). - Condition: Evaluated before each iteration. If
true, the loop body executes. Iffalse, the loop terminates. (e.g.,i < 10;). - Update (Increment/Decrement): Executed at the end of each iteration, typically modifying the loop counter (e.g.,
i++).
- Initialization: Executed only once at the beginning of the loop. Usually declares and initializes a loop counter variable (e.g.,
for (initialization; condition; update) { // Code to be executed repeatedly } - The
- The
whileLoop:- The
whileloop is ideal when you want to repeat a block of code as long as a certain condition remains true, and you might not know in advance how many times it will run. - Structure: It consists of a single boolean
condition.- The
conditionis evaluated before each iteration. - If
true, the loop body executes. - If
false, the loop terminates.
- The
while (condition) { // Code to be executed repeatedly // IMPORTANT: Must contain logic that eventually makes 'condition' false! } - The
- Avoiding Infinite Loops: This is critical! An infinite loop is a loop whose condition never becomes false. Your program will get stuck, consuming resources, and you'll usually have to force-quit it.
- For
forloops: Ensure yourupdatestatement correctly moves the loop counter towards the condition becoming false. - For
whileloops: Ensure that the loop body contains at least one statement that modifies a variable involved in thecondition, eventually making theconditionfalse.
- For
2. Professional Code: Loops in Action
Example 7.1: Basic for Loops
// Chapter7/ForLoopBasics.java
public class ForLoopBasics {
public static void main(String[] args) {
System.out.println("--- Counting Up (1 to 5) ---");
// Loop to count from 1 to 5
for (int i = 1; i <= 5; i++) {
System.out.println("Count: " + i);
}
System.out.println("\n--- Counting Down (10 to 0 by 2) ---");
// Loop to count down from 10 to 0, decrementing by 2
for (int j = 10; j >= 0; j -= 2) { // j -= 2 is same as j = j - 2
System.out.println("Countdown: " + j);
}
System.out.println("\n--- Calculating Sum (1 to 10) ---");
// Calculate the sum of numbers from 1 to 10
int sum = 0;
for (int k = 1; k <= 10; k++) {
sum += k; // sum += k is same as sum = sum + k
}
System.out.println("Sum of 1 to 10: " + sum); // Output: 55
}
}
Example 7.2: Basic while Loops
// Chapter7/WhileLoopBasics.java
public class WhileLoopBasics {
public static void main(String[] args) {
System.out.println("--- Simple While Loop (Counting) ---");
int count = 0; // Initialization
while (count < 5) { // Condition
System.out.println("While Count: " + count);
count++; // Update: crucial for avoiding infinite loop
}
System.out.println("\n--- Guessing Game (Conceptual) ---");
// Imagine a secret number is 7
int secretGuess = 7;
int userGuess = 0; // Initialize with a non-secret value
int attempts = 0;
// This would normally involve user input (Chapter 8), but for now, we simulate
// Let's say user guesses 3, then 5, then 7
int[] simulatedGuesses = {3, 5, 7};
int guessIndex = 0;
while (userGuess != secretGuess && guessIndex < simulatedGuesses.length) {
userGuess = simulatedGuesses[guessIndex]; // Simulate getting a guess
attempts++;
System.out.println("Attempt " + attempts + ": Guessed " + userGuess);
if (userGuess != secretGuess) {
System.out.println("Incorrect guess. Try again.");
}
guessIndex++; // Move to the next simulated guess
}
if (userGuess == secretGuess) {
System.out.println("Congratulations! You guessed the secret number " + secretGuess + " in " + attempts + " attempts.");
} else {
System.out.println("Ran out of simulated guesses without finding the secret number.");
}
}
}
Example 7.3: Avoiding Infinite Loops
// Chapter7/InfiniteLoopPrevention.java
public class InfiniteLoopPrevention {
public static void main(String[] args) {
// --- Correctly terminating while loop ---
int timer = 3;
System.out.println("--- Starting Timer ---");
while (timer > 0) {
System.out.println("Timer: " + timer);
timer--; // IMPORTANT: Decrementing 'timer' makes the condition eventually false
}
System.out.println("--- Timer Done ---");
// --- Example of a POTENTIAL infinite loop (do NOT run uncommented without care!) ---
/*
int neverEnding = 1;
while (neverEnding > 0) { // Condition will always be true
System.out.println("I'm stuck! " + neverEnding);
// neverEnding++; // If you uncomment this, it will run forever (or until int overflows)
// because neverEnding keeps growing and > 0 remains true.
// Even if you don't modify it, if it starts > 0, it stays > 0.
}
*/
// To fix the above: ensure 'neverEnding' is modified to eventually be <= 0.
// For example:
int controlledLoop = 1;
System.out.println("\n--- Controlled Loop ---");
while (controlledLoop <= 5) { // Condition: controlledLoop must be <= 5
System.out.println("Controlled: " + controlledLoop);
controlledLoop++; // This increments controlledLoop, making it eventually > 5.
}
System.out.println("--- Controlled Loop Done ---");
}
}
3. Line-by-Line Breakdown
Let's break down key lines from ForLoopBasics.java and WhileLoopBasics.java:
// From ForLoopBasics.java
for (int i = 1; i <= 5; i++) {
// 'int i = 1;': Initialization. A new integer variable 'i' is declared and set to 1. This happens once.
// 'i <= 5;': Condition. Before each iteration, this is checked. If 'i' is less than or equal to 5, the loop continues.
// 'i++': Update. After each iteration (after the code inside the curly braces runs), 'i' is incremented by 1.
System.out.println("Count: " + i);
// This line is executed in each iteration.
// Iteration 1: i=1 -> "Count: 1"
// Iteration 2: i=2 -> "Count: 2"
// ...
// Iteration 5: i=5 -> "Count: 5"
// After Iteration 5: i becomes 6. Condition (6 <= 5) is false. Loop terminates.
}
int sum = 0;
for (int k = 1; k <= 10; k++) {
sum += k; // This is a shorthand for sum = sum + k;
// Iteration 1: k=1, sum = 0 + 1 = 1
// Iteration 2: k=2, sum = 1 + 2 = 3
// Iteration 3: k=3, sum = 3 + 3 = 6
// ... and so on until k=10, sum becomes 55.
}
// From WhileLoopBasics.java
int count = 0;
while (count < 5) {
// 'while (count < 5)': Condition. This is checked at the start of each potential iteration.
// As long as 'count' is less than 5, the code inside the loop will execute.
System.out.println("While Count: " + count);
count++;
// 'count++': This is the update step. It's *inside* the loop body.
// This statement is absolutely critical. Without it, 'count' would always remain 0,
// the condition 'count < 5' would always be true, and the loop would run forever (infinite loop).
}
// Iteration 1: count=0. (0 < 5) is true. Prints "While Count: 0". count becomes 1.
// Iteration 2: count=1. (1 < 5) is true. Prints "While Count: 1". count becomes 2.
// ...
// Iteration 5: count=4. (4 < 5) is true. Prints "While Count: 4". count becomes 5.
// After Iteration 5: count=5. (5 < 5) is false. Loop terminates.
4. Clean Code Pro-Tips
- Choose the Right Loop:
- Use
forwhen you know the number of iterations in advance (e.g., iterating a fixed number of times, or over a collection with a known size). - Use
whilewhen the number of iterations is uncertain and depends on a condition being met (e.g., waiting for user input, processing data until a specific value is found).
- Use
- Prevent Infinite Loops: Always double-check that your
whileloop's body modifies the variables involved in its condition, ensuring it will eventually becomefalse. Forforloops, ensure the update statement correctly progresses towards the termination condition. - Loop Variable Scope: Variables declared in the
forloop's initialization (e.g.,int i = 0;) are scoped only to that loop. They cannot be accessed outside the loop after it finishes. Variables declared before awhileloop (e.g.,int count = 0;) are accessible after the loop finishes. - Clear Loop Conditions: Make your loop conditions as clear and simple as possible. Avoid overly complex boolean expressions within the loop condition if they can be simplified.
- Indentation: Proper indentation is crucial for loop readability, showing which code belongs to the loop body.
5. Unsolved Exercise: Repetitive Tasks
Let's practice both types of loops with some common scenarios.
- Create a class named
RepetitiveTasks. forloop task: Use aforloop to print all even numbers from2up to20(inclusive).whileloop task: Use awhileloop to simulate a countdown from5down to1. After the countdown, print "Lift off!".forloop task (Challenge): Use aforloop to print a multiplication table for the number7(from7 * 1to7 * 10).
6. Complete Solution: Repetitive Tasks
// Chapter7/RepetitiveTasks.java
public class RepetitiveTasks {
public static void main(String[] args) {
// 2. For loop to print even numbers from 2 to 20
System.out.println("--- Even Numbers (2-20) ---");
for (int i = 2; i <= 20; i += 2) { // Start at 2, increment by 2
System.out.println(i);
}
System.out.println("---------------------------\n");
// 3. While loop for a countdown
System.out.println("--- Countdown ---");
int countdown = 5;
while (countdown >= 1) { // Condition: as long as countdown is 1 or more
System.out.println(countdown);
countdown--; // Decrement: crucial to make the condition eventually false
}
System.out.println("Lift off!");
System.out.println("-----------------\n");
// 4. For loop for multiplication table of 7
System.out.println("--- Multiplication Table for 7 ---");
int multiplier = 7;
for (int i = 1; i <= 10; i++) {
System.out.println(multiplier + " x " + i + " = " + (multiplier * i));
}
System.out.println("----------------------------------");
}
}
Chapter 8: The Scanner Masterclass.
1. Brief Theory: Getting User Input
Up until now, our programs have been pretty static. They print information, perform calculations, and make decisions based on values we hardcoded directly into the program. But real-world applications need to interact with users! This is where getting user input comes in.
- The
ScannerClass: Java provides theScannerclass (found in thejava.utilpackage) to read input from various sources, including the console (keyboard).- Import Statement: Since
Scanneris not part of the core language, you need to explicitly tell Java where to find it. This is done with animportstatement at the very top of your.javafile, before the class declaration:import java.util.Scanner;. - Creating a
ScannerObject: To useScanner, you need to create an instance (an object) of it. You typically passSystem.into its constructor, which represents the standard input stream (your keyboard).Scanner scanner = new Scanner(System.in);
- Import Statement: Since
- Common
ScannerMethods for Input:nextInt(): Reads the next token as anint.nextDouble(): Reads the next token as adouble.nextBoolean(): Reads the next token as aboolean.next(): Reads the next word (a sequence of non-whitespace characters) as aString.nextLine(): Reads the entire line of input until the user presses Enter, including any spaces, and returns it as aString.
- The Essential
sc.nextLine()Buffer Fix (and why it's needed): This is a common gotcha for beginners!- When you use
nextInt(),nextDouble(),nextBoolean(), ornext(), these methods only consume the actual data you typed. They leave the newline character (theEnterkey press) in the input buffer. - If you then immediately call
nextLine()after one of these methods,nextLine()will see that leftover newline character in the buffer and consume it immediately, thinking it has read an empty line. Your program won't pause to wait for your actual line of text input. - The Fix: After calling
nextInt(),nextDouble(), etc., always add an extrascanner.nextLine();call to consume the leftover newline character before you try to read an actual line of text.
- When you use
- Closing the
Scanner: It's good practice to close theScannerobject when you are finished using it to release system resources. You do this withscanner.close();.
2. Professional Code: Scanner in Action
Example 8.1: Reading Various Primitive Types
// Chapter8/BasicInput.java
import java.util.Scanner; // Don't forget this import!
public class BasicInput {
public static void main(String[] args) {
// Create a Scanner object to read input from the console
Scanner scanner = new Scanner(System.in);
System.out.println("--- User Input Practice ---");
// Read an integer
System.out.print("Enter your age: "); // print() keeps cursor on same line
int age = scanner.nextInt();
System.out.println("You entered age: " + age);
// Consume the leftover newline character after nextInt()
scanner.nextLine();
// Read a line of text (e.g., full name)
System.out.print("Enter your full name: ");
String fullName = scanner.nextLine();
System.out.println("You entered name: " + fullName);
// Read a double
System.out.print("Enter your GPA (e.g., 3.8): ");
double gpa = scanner.nextDouble();
System.out.println("You entered GPA: " + gpa);
// Consume the leftover newline character after nextDouble()
scanner.nextLine();
// Read a single word (e.g., favorite color)
System.out.print("Enter your favorite color (single word): ");
String color = scanner.next();
System.out.println("You entered color: " + color);
// No nextLine() needed after next() if it's the last input or followed by another nextX()
System.out.println("--- Input Reading Complete ---");
// Close the scanner to release system resources
scanner.close();
}
}
Example 8.2: Demonstrating and Fixing the nextLine() Buffer Issue
// Chapter8/ScannerBufferIssue.java
import java.util.Scanner;
public class ScannerBufferIssue {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("--- Demonstrating Scanner nextLine() Issue ---");
System.out.print("Enter your favorite number (int): ");
int favNumber = scanner.nextInt(); // Reads 123, leaves '\n' in buffer
System.out.println("Your favorite number is: " + favNumber);
// *** PROBLEM HERE: nextLine() reads the leftover '\n' from nextInt() ***
// If the line below is uncommented, it will NOT pause for input.
// System.out.print("Now enter your favorite quote: ");
// String favQuoteProblem = scanner.nextLine();
// System.out.println("Problematic quote: " + favQuoteProblem + " (This might be empty!)");
// --- THE FIX ---
System.out.print("Enter your age (int): ");
int age = scanner.nextInt();
System.out.println("Your age is: " + age);
// Consume the leftover newline character explicitly!
scanner.nextLine(); // This reads and discards the '\n' from the 'age' input
System.out.print("Now enter your favorite city (full line): ");
String favCity = scanner.nextLine(); // Now it correctly waits for and reads your city
System.out.println("Your favorite city is: " + favCity);
System.out.println("--- Issue Demonstration Complete ---");
scanner.close();
}
}
Example 8.3: Simple Calculator with User Input
// Chapter8/SimpleCalculator.java
import java.util.Scanner;
public class SimpleCalculator {
public static void main(String[] args) {
Scanner inputScanner = new Scanner(System.in);
System.out.println("--- Simple Calculator ---");
System.out.print("Enter first number (double): ");
double num1 = inputScanner.nextDouble();
System.out.print("Enter second number (double): ");
double num2 = inputScanner.nextDouble();
// Consume the leftover newline character after nextDouble()
inputScanner.nextLine();
System.out.print("Enter an operator (+, -, *, /): ");
String operator = inputScanner.nextLine(); // Reading operator as a string
double result = 0;
boolean isValidOperation = true;
switch (operator) {
case "+" -> result = num1 + num2;
case "-" -> result = num1 - num2;
case "*" -> result = num1 * num2;
case "/" -> {
if (num2 != 0) {
result = num1 / num2;
} else {
System.out.println("Error: Division by zero is not allowed.");
isValidOperation = false;
}
}
default -> {
System.out.println("Error: Invalid operator.");
isValidOperation = false;
}
}
if (isValidOperation) {
System.out.printf("Result: %.2f %s %.2f = %.2f\n", num1, operator, num2, result);
} else {
System.out.println("Calculation could not be performed due to an error.");
}
System.out.println("--- Calculator Done ---");
inputScanner.close();
}
}
3. Line-by-Line Breakdown
Let's break down key lines from BasicInput.java and ScannerBufferIssue.java:
// From BasicInput.java
import java.util.Scanner;
// This line tells the Java compiler that we want to use the 'Scanner' class,
// which is located in the 'java.util' package. Without this, the compiler wouldn't know what 'Scanner' is.
Scanner scanner = new Scanner(System.in);
// 'Scanner': This is the type of variable we are declaring.
// 'scanner': This is the name of our Scanner variable (object reference).
// 'new Scanner(System.in)': This creates a new 'Scanner' object.
// 'new': The keyword to create a new object.
// 'System.in': This specifies the input source. 'System.in' refers to the standard input stream,
// which by default is your keyboard.
System.out.print("Enter your age: ");
// 'System.out.print()': Displays the message to the console.
// Unlike 'println()', 'print()' does NOT add a new line character at the end,
// so the user's input will appear on the same line as the prompt.
int age = scanner.nextInt();
// 'scanner.nextInt()': This method reads the next integer value typed by the user from the input stream.
// It parses the string representation of the number into an 'int' data type.
// The program pauses here until the user types something and presses Enter.
// 'int age = ...': The integer value read is then assigned to the 'age' variable.
scanner.nextLine();
// This is the crucial line for the buffer fix.
// It reads and discards the leftover newline character ('\n') that was generated when the user pressed Enter
// after typing their age for 'nextInt()'. This clears the buffer for the next 'nextLine()' call.
String fullName = scanner.nextLine();
// 'scanner.nextLine()': This method reads *all* characters until it encounters a newline character ('\n').
// It then consumes that newline character and returns the entire line of text as a 'String'.
// This is typically used when you want to read a phrase, sentence, or any input that might
// contain spaces.
scanner.close();
// This method closes the 'Scanner' object, releasing any underlying system resources (like the keyboard input stream).
// It's good practice to close resources when you are done with them.
4. Clean Code Pro-Tips
- Always Import
Scanner: Don't forgetimport java.util.Scanner;at the top of your file. - Prompt the User Clearly: Always print a clear message (a "prompt") to the console before asking for input. Tell the user what kind of input you expect (e.g., "Enter your name:", "Enter a number:"). Use
System.out.print()for prompts so the cursor stays on the same line. - Handle the
nextLine()Buffer Issue: This is perhaps the most important tip for beginners usingScanner. Whenever you read a primitive type (nextInt(),nextDouble(),next(), etc.) and then need to read a full line of text (nextLine()), insert an extrascanner.nextLine();to consume the leftover newline. - Close the
Scanner: Usescanner.close();when you are done with all your input to prevent resource leaks. A common place for this is at the very end of yourmainmethod. - Meaningful Variable Names: Store user input in variables with names that reflect what they represent (e.g.,
userName,userAge,price,choice).
5. Unsolved Exercise: Personalized Greeting
Let's build a program that greets a user and asks for some personal details.
- Create a class named
PersonalizedGreeting. - Import the
Scannerclass. - Create a
Scannerobject. - Ask the user for their
name(full line, including spaces) and store it in aStringvariable. - Ask the user for their
ageand store it in anintvariable. - Ask the user for their
favorite numberand store it in adoublevariable. - Crucially: Ensure that the
nextLine()buffer fix is applied correctly after reading theageandfavorite number, so that any subsequentnextLine()calls work as expected (though we won't have any after the name in this specific example, it's good practice to get into the habit). - Print a personalized message using all the collected information, for example: "Hello, [Name]! You are [Age] years old and your favorite number is [Favorite Number]."
- Close the
Scannerobject.
6. Complete Solution: Personalized Greeting
// Chapter8/PersonalizedGreeting.java
import java.util.Scanner; // Don't forget this!
public class PersonalizedGreeting {
public static void main(String[] args) {
// 3. Create a Scanner object
Scanner keyboardInput = new Scanner(System.in);
System.out.println("--- Personalized Greeting Program ---");
// 4. Ask for the user's name (full line)
System.out.print("Please enter your full name: ");
String userName = keyboardInput.nextLine();
// 5. Ask for the user's age
System.out.print("Please enter your age: ");
int userAge = keyboardInput.nextInt();
// 7. Essential: Consume the leftover newline character after nextInt()
keyboardInput.nextLine();
// 6. Ask for the user's favorite number
System.out.print("Please enter your favorite number (e.g., 3.14): ");
double favoriteNumber = keyboardInput.nextDouble();
// 7. Essential: Consume the leftover newline character after nextDouble()
// Good practice, even if no nextLine() follows directly, for consistency.
keyboardInput.nextLine();
// 8. Print a personalized message
System.out.printf("\nHello, %s! You are %d years old and your favorite number is %.2f.\n",
userName, userAge, favoriteNumber);
System.out.println("Nice to meet you!");
System.out.println("--- Program End ---");
// 9. Close the Scanner object
keyboardInput.close();
}
}
Book 1: Part 2 (Advanced Foundations)
Chapter 9: One-Dimensional Arrays.
1. Quick Theory: Grouping Your Data
Imagine you need to store the scores of 100 students. Would you create 100 separate int variables like student1Score, student2Score, etc.? That would be incredibly tedious and unmanageable! This is where arrays come to the rescue.
An array is a special type of variable that can hold multiple values of the same data type under a single name. Think of it as a list or a sequence of elements, each accessible by an index (its position). In Java, array indices are 0-based, meaning the first element is at index 0, the second at 1, and so on. Arrays are objects, meaning their data is stored on the Heap, and your array variable holds a reference to that memory location. The "Why": Arrays provide an efficient way to store and access a fixed-size collection of homogeneous data.
2. Code Examples: Arrays in Action
Example 9.1: Declaring, Initializing, and Accessing with for loop
// Chapter9/StudentScores.java
public class StudentScores {
public static void main(String[] args) {
// 1. Array Declaration: How to tell Java you'll have an array
// Option A: Declare without size immediately (type[] arrayName;)
int[] scores;
// Option B: Declare and allocate size (type[] arrayName = new type[size];)
// This array can hold 5 integer scores. Default value for int is 0.
scores = new int[5];
// 2. Initializing elements (assigning values)
scores[0] = 85; // First element at index 0
scores[1] = 92; // Second element at index 1
scores[2] = 78; // Third element
scores[3] = 95; // Fourth element
scores[4] = 60; // Fifth (and last) element at index 4 (size - 1)
// What if we try to access an index out of bounds?
// scores[5] = 100; // This would cause an ArrayIndexOutOfBoundsException at runtime!
// 3. Accessing elements using a standard 'for' loop
System.out.println("--- Student Scores (using standard for loop) ---");
for (int i = 0; i < scores.length; i++) {
// 'scores.length' is a built-in property that gives the size of the array.
// Loop from 0 up to (but not including) scores.length to cover all indices.
System.out.println("Student " + (i + 1) + " Score: " + scores[i]);
}
// 4. Declaring and initializing an array in one line
String[] studentNames = {"Alice", "Bob", "Charlie", "Diana"};
System.out.println("\n--- Student Names ---");
System.out.println("First student: " + studentNames[0]); // Accessing "Alice"
System.out.println("Number of students: " + studentNames.length); // Output: 4
}
}
Example 9.2: Iterating with the Modern for-each Loop
// Chapter9/ForEachLoopExample.java
public class ForEachLoopExample {
public static void main(String[] args) {
// Array of product prices
double[] productPrices = {15.99, 23.50, 5.00, 10.75, 45.20};
System.out.println("--- Product Prices (using for-each loop) ---");
// The 'for-each' loop (enhanced for loop) is perfect for iterating over
// all elements of an array or collection when you don't need the index.
for (double price : productPrices) {
// For each 'double' element in 'productPrices', assign its value to 'price'
// and execute the loop body.
System.out.println("Price: $" + price);
}
// Calculating total price using for-each
double totalPrice = 0;
for (double price : productPrices) {
totalPrice += price; // Add each price to the total
}
System.out.printf("Total Price of all products: $%.2f\n", totalPrice);
// Array of characters (e.g., initials)
char[] initials = {'J', 'D', 'M', 'S'};
System.out.println("\n--- User Initials ---");
for (char initial : initials) {
System.out.print(initial + " "); // Print initials on a single line
}
System.out.println(); // Add a newline at the end
}
}
3. Line-by-Line Breakdown
Let's dissect StudentScores.java:
int[] scores;
// 'int[]': This declares a variable named 'scores' that will hold an array of integers.
// The '[]' indicates it's an array type.
scores = new int[5];
// 'new int[5]': This creates a new array object on the Heap that can hold 5 integer values.
// All elements are automatically initialized to their default value (0 for int).
// 'scores = ...': The reference to this newly created array object is assigned to the 'scores' variable.
scores[0] = 85;
// 'scores[0]': This accesses the element at index 0 (the first element) of the 'scores' array.
// '= 85;': The value 85 is assigned to this specific element.
for (int i = 0; i < scores.length; i++) {
// 'int i = 0;': Loop counter 'i' starts at 0, which is the first valid index for arrays.
// 'i < scores.length;': Loop continues as long as 'i' is less than the total number of elements.
// If scores.length is 5, 'i' will go from 0, 1, 2, 3, 4. When 'i' becomes 5,
// the condition (5 < 5) is false, and the loop stops, preventing
// ArrayIndexOutOfBoundsException.
// 'i++': Increment 'i' after each iteration to move to the next element.
System.out.println("Student " + (i + 1) + " Score: " + scores[i]);
// 'scores[i]': Accesses the element at the current index 'i'.
// '(i + 1)': We add 1 to 'i' when printing to make it more human-readable (Student 1, Student 2, etc., instead of Student 0).
}
And from ForEachLoopExample.java:
for (double price : productPrices) {
// 'for (double price : productPrices)': This is the 'for-each' loop syntax.
// 'double price': In each iteration, the current element from 'productPrices' will be assigned to this variable 'price'.
// 'productPrices': This is the array (or any iterable collection) that we want to loop through.
System.out.println("Price: $" + price);
// Inside the loop, 'price' holds the value of the current element.
// The loop automatically iterates over all elements from start to finish.
}
4. Clean Code Pro-Tips
- Initialize Arrays: Always initialize your arrays, either by specifying a size (
new int[5]) or by providing initial values ({85, 92, ...}). Uninitialized array variables arenull. - Use
for-eachwhen appropriate: If you need to process every element in an array and don't need to know its index, thefor-eachloop is more concise and less error-prone than a traditionalforloop. - Use
array.length: Always use the.lengthproperty to get the size of an array. Never hardcode the size into your loop conditions, as this can lead toArrayIndexOutOfBoundsExceptionif the array size changes. - Meaningful Names: Array names should be plural (e.g.,
studentScores,productPrices) to indicate they hold multiple items.
5. Unsolved Exercise: Daily Temperatures
Your task is to create a program that manages daily temperatures.
- Create a class named
DailyTemperatures. - Declare an array named
temperaturesof typedoubleto store 7 daily temperatures. - Initialize the array with arbitrary temperatures for 7 days (e.g.,
20.5, 22.1, 19.8, 23.0, 25.4, 21.0, 18.7). - Use a standard
forloop to print each day's temperature, labeling them as "Day 1", "Day 2", etc. - Use a
for-eachloop to calculate and print the average temperature for the week.
6. Complete Solution: Daily Temperatures
// Chapter9/DailyTemperatures.java
public class DailyTemperatures {
public static void main(String[] args) {
// 2. Declare and 3. Initialize the array of 7 daily temperatures
double[] temperatures = {20.5, 22.1, 19.8, 23.0, 25.4, 21.0, 18.7};
System.out.println("--- Weekly Temperatures ---");
// 4. Print each day's temperature using a standard 'for' loop
for (int i = 0; i < temperatures.length; i++) {
System.out.println("Day " + (i + 1) + ": " + temperatures[i] + "°C");
}
// 5. Calculate and print the average temperature using a 'for-each' loop
double totalTemperature = 0;
for (double temp : temperatures) {
totalTemperature += temp; // Add each temperature to the total
}
double averageTemperature = totalTemperature / temperatures.length;
System.out.printf("\nAverage weekly temperature: %.2f°C\n", averageTemperature);
System.out.println("---------------------------");
}
}
Chapter 10: The Arrays Utility Class.
1. Quick Theory: Array Superpowers
Working with arrays is common, so common that Java provides a specialized utility class to make array operations easier: java.util.Arrays. This class offers static methods (methods you call directly on the class, not an object of the class) that perform common tasks like sorting, searching, and converting arrays to strings.
The "Why": Instead of writing your own sorting algorithm (which is complex!) or a loop to print every element, Arrays provides battle-tested, efficient implementations for you. This saves time, reduces errors, and keeps your code cleaner and more professional. It's a prime example of reusing existing functionality.
2. Code Examples: Arrays Utility in Action
Example 10.1: Sorting and Printing Arrays
// Chapter10/ArraySortingAndPrinting.java
import java.util.Arrays; // Don't forget to import the Arrays utility class!
public class ArraySortingAndPrinting {
public static void main(String[] args) {
int[] numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
String[] fruits = {"orange", "apple", "grape", "banana", "kiwi"};
System.out.println("--- Original Arrays ---");
// Arrays.toString() converts an array into a human-readable String (e.g., "[5, 2, 8, ...]")
System.out.println("Numbers: " + Arrays.toString(numbers));
System.out.println("Fruits: " + Arrays.toString(fruits));
// 1. Sorting an array: Arrays.sort()
// This method sorts the elements of the array in ascending order (modifies the original array).
Arrays.sort(numbers);
Arrays.sort(fruits);
System.out.println("\n--- Sorted Arrays ---");
System.out.println("Numbers (Sorted): " + Arrays.toString(numbers));
System.out.println("Fruits (Sorted): " + Arrays.toString(fruits));
// Sorting a portion of an array (optional, but good to know)
int[] partialSortArray = {10, 30, 20, 50, 40};
System.out.println("\nOriginal Partial Sort Array: " + Arrays.toString(partialSortArray));
// Sorts from index 1 (inclusive) to index 4 (exclusive)
Arrays.sort(partialSortArray, 1, 4);
System.out.println("Partially Sorted Array (indices 1 to 3): " + Arrays.toString(partialSortArray));
// Output: [10, 20, 30, 50, 40] - Note: 50 is still at index 3 because 4 is exclusive
}
}
Example 10.2: Comparing Arrays and Filling Arrays
// Chapter10/ArrayComparisonAndFill.java
import java.util.Arrays;
public class ArrayComparisonAndFill {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = {1, 2, 3, 4, 5};
int[] arr3 = {5, 4, 3, 2, 1};
int[] arr4 = {1, 2, 3};
System.out.println("--- Array Comparison ---");
// 1. Comparing arrays: Arrays.equals()
// This method checks if two arrays have the same number of elements AND
// if all corresponding elements are equal in value and order.
System.out.println("arr1 equals arr2? " + Arrays.equals(arr1, arr2)); // Output: true
System.out.println("arr1 equals arr3? " + Arrays.equals(arr1, arr3)); // Output: false (order differs)
System.out.println("arr1 equals arr4? " + Arrays.equals(arr1, arr4)); // Output: false (length differs)
// What about '=='? Remember, '==' compares references (memory addresses) for objects.
System.out.println("arr1 == arr2? " + (arr1 == arr2)); // Output: false (they are different objects)
// 2. Filling an array: Arrays.fill()
int[] newArray = new int[5]; // Creates an array of 5 integers, all initialized to 0.
System.out.println("\nNew Array (initial): " + Arrays.toString(newArray)); // Output: [0, 0, 0, 0, 0]
Arrays.fill(newArray, 100); // Fills all elements of newArray with the value 100.
System.out.println("New Array (filled with 100): " + Arrays.toString(newArray)); // Output: [100, 100, 100, 100, 100]
// You can also fill a portion
int[] anotherArray = new int[5];
Arrays.fill(anotherArray, 1, 4, 50); // Fills from index 1 (inclusive) to 4 (exclusive) with 50
System.out.println("Another Array (partially filled): " + Arrays.toString(anotherArray)); // Output: [0, 50, 50, 50, 0]
}
}
3. Line-by-Line Breakdown
Let's break down ArraySortingAndPrinting.java:
import java.util.Arrays;
// This import statement makes all the static methods of the 'Arrays' class available
// for use in this file without having to prefix them with 'java.util.Arrays'.
System.out.println("Numbers: " + Arrays.toString(numbers));
// 'Arrays.toString(numbers)': This static method takes an array (in this case, 'numbers')
// as an argument and returns a 'String' representation of its contents.
// It's incredibly useful for debugging and printing array values easily.
Arrays.sort(numbers);
// 'Arrays.sort(numbers)': This static method sorts the elements of the 'numbers' array in place.
// "In place" means it modifies the original 'numbers' array directly; it doesn't return a new sorted array.
// For primitive types and String, it sorts in natural (ascending) order.
And from ArrayComparisonAndFill.java:
System.out.println("arr1 equals arr2? " + Arrays.equals(arr1, arr2));
// 'Arrays.equals(arr1, arr2)': This static method compares two arrays.
// It returns 'true' if both arrays are of the same type, have the same length,
// and all corresponding elements at each index are equal. Otherwise, it returns 'false'.
System.out.println("arr1 == arr2? " + (arr1 == arr2));
// '(arr1 == arr2)': This compares the memory addresses (references) of the two array objects.
// Even though 'arr1' and 'arr2' have identical content, they are two separate objects
// created with 'new int[] { ... }' in different memory locations.
// Therefore, their references are different, and '==' returns 'false'.
Arrays.fill(newArray, 100);
// 'Arrays.fill(newArray, 100)': This static method sets every element in the 'newArray' to the value 100.
// It's a convenient way to initialize or reset all elements of an array to a single value.
4. Clean Code Pro-Tips
- Leverage Utility Classes: Always check if a utility class (like
Arrays,Collections,Math, etc.) already provides a method for a common task before writing your own implementation. This saves time and ensures robust, optimized code. - Understand
equals()vs.==for Arrays: Be absolutely clear thatArrays.equals()compares the contents of arrays, while==compares their references (memory addresses). Using==for content comparison is a very common bug. - Immutability for
toString: Remember thatArrays.toString()returns a new string representation; it doesn't modify the array itself. - In-Place Sorting: Be aware that
Arrays.sort()modifies the original array. If you need to keep the original array unsorted, you'd have to make a copy of it first.
5. Unsolved Exercise: Student Gradebook
You are managing a small gradebook for a class.
- Create a class named
StudentGradebook. - Declare an
intarray namedgradesand initialize it with 5 arbitrary student grades (e.g.,85, 72, 95, 68, 80). - Print the original grades array using
Arrays.toString(). - Sort the
gradesarray in ascending order. - Print the sorted
gradesarray. - Create a second
intarray namedpassingGradesof the same size, and fill all its elements with a passing score, say70. - Print the
passingGradesarray. - Compare the original
gradesarray (which is now sorted) with thepassingGradesarray usingArrays.equals()and print the result. Explain why you get that result.
6. Complete Solution: Student Gradebook
// Chapter10/StudentGradebook.java
import java.util.Arrays; // Needed for Arrays utility methods
public class StudentGradebook {
public static void main(String[] args) {
// 2. Declare and initialize the grades array
int[] grades = {85, 72, 95, 68, 80};
System.out.println("--- Student Gradebook ---");
// 3. Print the original grades array
System.out.println("Original Grades: " + Arrays.toString(grades));
// 4. Sort the grades array
Arrays.sort(grades);
// 5. Print the sorted grades array
System.out.println("Sorted Grades: " + Arrays.toString(grades));
// 6. Create and fill the passingGrades array
int[] passingGrades = new int[5];
Arrays.fill(passingGrades, 70); // Fill all elements with 70
// 7. Print the passingGrades array
System.out.println("Passing Grades: " + Arrays.toString(passingGrades));
// 8. Compare the sorted grades array with the passingGrades array
boolean areEqual = Arrays.equals(grades, passingGrades);
System.out.println("\nAre sorted grades and passing grades arrays equal? " + areEqual);
// Explanation: This will likely be 'false' unless all original grades coincidentally became 70 after sorting,
// which is extremely unlikely. Arrays.equals() checks if elements at *each corresponding index* are identical.
// Even if some grades were 70, the arrays as a whole would only be equal if all 5 elements matched perfectly.
System.out.println("-------------------------");
}
}
Chapter 11: Multidimensional Arrays (Matrices).
1. Quick Theory: Tables and Grids
Sometimes, a single list isn't enough to represent your data. What if you need to store data in a grid, like a spreadsheet, a game board, or coordinates? That's when multidimensional arrays, often called matrices (especially 2D arrays), become essential.
A 2D array is essentially an "array of arrays." Each element of the outer array is itself another array. For example, a int[3][4] array means an array with 3 rows, where each row is an array of 4 integers. Like 1D arrays, multidimensional arrays are objects on the Heap. The "Why": They provide a structured way to model tabular data or spatial relationships, making it easier to manage complex datasets in a more intuitive, row-column format.
2. Code Examples: Matrices in Action
Example 11.1: Declaring, Initializing, and Printing a 2D Array
// Chapter11/TwoDArrayBasic.java
public class TwoDArrayBasic {
public static void main(String[] args) {
// 1. Declare and initialize a 2D array (3 rows, 4 columns)
// int[row][column]
int[][] matrix = {
{1, 2, 3, 4}, // Row 0
{5, 6, 7, 8}, // Row 1
{9, 10, 11, 12} // Row 2
};
// 2. Accessing elements
System.out.println("Element at [0][0]: " + matrix[0][0]); // Output: 1
System.out.println("Element at [1][2]: " + matrix[1][2]); // Output: 7 (second row, third column)
System.out.println("Element at [2][3]: " + matrix[2][3]); // Output: 12 (third row, fourth column)
// 3. Getting dimensions
System.out.println("Number of rows: " + matrix.length); // Output: 3 (length of the outer array)
System.out.println("Number of columns (in row 0): " + matrix[0].length); // Output: 4 (length of the first inner array)
// 4. Printing the 2D array using nested 'for' loops
System.out.println("\n--- Printing Matrix ---");
for (int i = 0; i < matrix.length; i++) { // Outer loop for rows
for (int j = 0; j < matrix[i].length; j++) { // Inner loop for columns in the current row 'i'
System.out.print(matrix[i][j] + "\t"); // Print element and a tab for spacing
}
System.out.println(); // Move to the next line after each row
}
System.out.println("-----------------------");
}
}
Example 11.2: A Simple Game Board (Dynamic Creation)
// Chapter11/GameBoard.java
public class GameBoard {
public static void main(String[] args) {
// Create an empty 5x5 character game board
int rows = 5;
int cols = 5;
char[][] board = new char[rows][cols]; // All elements initialized to default char value '\u0000' (null character)
// Initialize the board with empty spaces or a specific character
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
board[i][j] = '-'; // Use '-' to represent empty cells
}
}
// Place some game pieces
board[0][0] = 'X'; // Player X at top-left
board[2][2] = 'O'; // Player O at center
board[4][0] = 'X'; // Another X
// Print the game board
System.out.println("--- Game Board ---");
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
System.out.print(board[i][j] + " "); // Print element with a space
}
System.out.println(); // New line for each row
}
System.out.println("------------------");
// Example: Update a cell
board[0][1] = 'O';
System.out.println("\n--- Board After Update ---");
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
System.out.println("--------------------------");
}
}
3. Line-by-Line Breakdown
Let's dissect TwoDArrayBasic.java:
int[][] matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 'int[][]': Declares a variable 'matrix' that will hold a 2D array of integers.
// '{ { ... }, { ... } }': This is shorthand for declaring and initializing a 2D array.
// The outer curly braces define the entire array.
// Each inner curly brace defines a row (an inner array).
// Here, it creates an array with 3 inner arrays, each having 4 integers.
System.out.println("Element at [1][2]: " + matrix[1][2]);
// 'matrix[1]': Accesses the array at index 1 (the second row: {5, 6, 7, 8}).
// 'matrix[1][2]': From that inner array, accesses the element at index 2 (the third column: 7).
for (int i = 0; i < matrix.length; i++) { // Outer loop for rows
// 'matrix.length': Gives the number of *rows* (the length of the outer array).
// In this case, it's 3. 'i' will go from 0 to 2.
for (int j = 0; j < matrix[i].length; j++) { // Inner loop for columns
// 'matrix[i]': Refers to the current row (the inner array at index 'i').
// 'matrix[i].length': Gives the number of *columns* in the current row 'i'.
// For a regular (non-jagged) 2D array, this will be the same for all rows (e.g., 4).
// 'j' will go from 0 to 3 for each row.
System.out.print(matrix[i][j] + "\t");
// 'matrix[i][j]': Accesses the element at the current row 'i' and column 'j'.
}
System.out.println(); // After printing all columns of a row, move to the next line.
}
4. Clean Code Pro-Tips
- Visualize: When working with 2D arrays, always mentally (or literally) draw out the grid to keep track of rows and columns, especially during indexing.
- Clear Loop Control: In nested loops, ensure the outer loop controls the rows and the inner loop controls the columns. Use
matrix.lengthfor the number of rows andmatrix[i].lengthfor the number of columns in the current row. - Square vs. Jagged: Most common are "square" or "rectangular" 2D arrays where all rows have the same number of columns. Java also supports "jagged" arrays where inner arrays can have different lengths (
new int[3][]thennew int[0]=...,new int[1]=...). Stick to rectangular for now. - Descriptive Variable Names: Use names like
matrix,grid,boardfor your 2D arrays to convey their purpose.
5. Unsolved Exercise: Classroom Seating Chart
You need to create a simple seating chart for a classroom.
- Create a class named
ClassroomSeating. - Declare a
String2D array namedseatingChartwith 3 rows and 4 columns. - Initialize the
seatingChartwith student names (e.g., "Alice", "Bob", "Charlie", "Diana" for the first row, then other names, or "EMPTY" if a seat is free). - Print the entire
seatingChartneatly, using nestedforloops. Each student name should take up some space, perhaps with a tab\t. - Change the student in seat
[1][1](second row, second column) to "NewStudent". - Print the updated
seatingChartto confirm the change.
6. Complete Solution: Classroom Seating Chart
// Chapter11/ClassroomSeating.java
public class ClassroomSeating {
public static void main(String[] args) {
// 2. Declare a String 2D array named seatingChart (3 rows, 4 columns)
// 3. Initialize the seatingChart with student names or "EMPTY"
String[][] seatingChart = {
{"Alice", "Bob", "Charlie", "Diana"},
{"Ethan", "Frank", "Grace", "Harry"},
{"Ivy", "Jake", "Karen", "Liam"}
};
System.out.println("--- Original Classroom Seating Chart ---");
// 4. Print the entire seatingChart using nested 'for' loops
for (int row = 0; row < seatingChart.length; row++) { // Iterate through rows
for (int col = 0; col < seatingChart[row].length; col++) { // Iterate through columns in the current row
// Print student name, padded to ensure alignment
System.out.print(String.format("%-10s", seatingChart[row][col])); // %-10s means left-align string in 10 chars
}
System.out.println(); // New line after each row
}
System.out.println("----------------------------------------");
// 5. Change the student in seat [1][1] (second row, second column)
seatingChart[1][1] = "NewStudent";
System.out.println("\n--- Updated Classroom Seating Chart ---");
// 6. Print the updated seatingChart
for (int row = 0; row < seatingChart.length; row++) {
for (int col = 0; col < seatingChart[row].length; col++) {
System.out.print(String.format("%-10s", seatingChart[row][col]));
}
System.out.println();
}
System.out.println("---------------------------------------");
}
}
Chapter 12: Methods (The Power of Reusability).
1. Quick Theory: Breaking Down Complexity
As your programs grow, putting all your code inside the main method becomes messy and hard to manage. This is where methods (also known as functions in other languages) come in. A method is a block of code that performs a specific task. You can define a method once and then "call" or "invoke" it multiple times from different parts of your program.
The "Why":
- Reusability (DRY - Don't Repeat Yourself): If you need to perform the same task multiple times, you write the code once in a method and call the method.
- Modularity & Organization: Breaking down a large problem into smaller, manageable methods makes your code easier to understand, debug, and maintain. Each method can focus on a single, well-defined task.
- Readability: A
mainmethod that calls several well-named methods is much easier to read and understand than a long block of code.
For now, we'll focus on public static methods.
public: Means the method can be accessed from anywhere.static: Means the method belongs to the class itself, not to an object of the class. This allows us to call it directly frommainwithout creating an object of our class.returnType: The type of data the method will send back to the caller (e.g.,int,double,String,boolean). If a method doesn't send back any value, its return type isvoid.methodName: A descriptive name for what the method does.parameters: A list of variables that the method accepts as input, enclosed in parentheses. These are optional.
The return statement is used in methods that have a non-void return type. It sends a value back to the code that called the method and immediately exits the method.
2. Code Examples: Building Your Own Methods
Example 12.1: Method with Parameters and a Return Value
// Chapter12/CalculatorMethods.java
public class CalculatorMethods {
public static void main(String[] args) {
System.out.println("--- Basic Calculator Methods ---");
// Calling the 'add' method and storing its returned value
int sum = add(10, 5); // 10 and 5 are "arguments"
System.out.println("Sum of 10 and 5: " + sum); // Output: 15
// Calling the 'subtract' method
double difference = subtract(20.5, 7.3);
System.out.printf("Difference of 20.5 and 7.3: %.2f\n", difference); // Output: 13.20
// Calling the 'multiply' method directly in println
System.out.println("Product of 4 and 6: " + multiply(4, 6)); // Output: 24
// Calling a void method
greetUser("Alice"); // "Alice" is the argument
greetUser("Bob");
// Using return value in a conditional
int num = 15;
if (isEven(num)) {
System.out.println(num + " is an even number.");
} else {
System.out.println(num + " is an odd number.");
}
}
// Method to add two integers
// 'public static': Access modifiers (visible everywhere, belongs to the class)
// 'int': Return type - this method will return an integer value
// 'add': Method name
// '(int a, int b)': Parameters - this method accepts two integers, named 'a' and 'b'
public static int add(int a, int b) {
int result = a + b;
return result; // 'return' keyword sends 'result' back to the caller
}
// Method to subtract two doubles
public static double subtract(double num1, double num2) {
// You can return the expression directly
return num1 - num2;
}
// Method to multiply two integers
public static int multiply(int x, int y) {
return x * y;
}
// Method with 'void' return type (doesn't return any value)
public static void greetUser(String name) { // Takes one String parameter
System.out.println("Hello, " + name + "! Welcome to Java methods.");
// No 'return' statement is explicitly needed for void methods at the end,
// but an empty 'return;' can be used to exit early.
}
// Method to check if a number is even
public static boolean isEven(int number) {
return number % 2 == 0; // Returns true if number is even, false otherwise
}
}
Example 12.2: Reusing Methods to Calculate Area
// Chapter12/AreaCalculator.java
public class AreaCalculator {
public static void main(String[] args) {
System.out.println("--- Area Calculator ---");
// Calculate area of a rectangle
double rectLength = 10.0;
double rectWidth = 5.0;
double rectangleArea = calculateRectangleArea(rectLength, rectWidth);
System.out.printf("Area of rectangle (%.1f x %.1f): %.2f\n", rectLength, rectWidth, rectangleArea);
// Calculate area of a circle
double circleRadius = 7.0;
double circleArea = calculateCircleArea(circleRadius);
System.out.printf("Area of circle (radius %.1f): %.2f\n", circleRadius, circleArea);
// Calculate area of a triangle
double triBase = 8.0;
double triHeight = 4.0;
double triangleArea = calculateTriangleArea(triBase, triHeight);
System.out.printf("Area of triangle (base %.1f, height %.1f): %.2f\n", triBase, triHeight, triangleArea);
System.out.println("-----------------------");
}
// Method to calculate the area of a rectangle
public static double calculateRectangleArea(double length, double width) {
if (length <= 0 || width <= 0) { // Basic validation
System.out.println("Error: Length and width must be positive.");
return 0.0; // Return a default/error value
}
return length * width;
}
// Method to calculate the area of a circle
public static double calculateCircleArea(double radius) {
if (radius <= 0) {
System.out.println("Error: Radius must be positive.");
return 0.0;
}
// Math.PI is a constant for the value of Pi
return Math.PI * radius * radius;
}
// Method to calculate the area of a triangle
public static double calculateTriangleArea(double base, double height) {
if (base <= 0 || height <= 0) {
System.out.println("Error: Base and height must be positive.");
return 0.0;
}
return 0.5 * base * height; // Or (base * height) / 2.0;
}
}
3. Line-by-Line Breakdown
Let's dissect CalculatorMethods.java:
public static int add(int a, int b) {
// 'public static': Standard modifiers for now.
// 'int': This specifies that the method 'add' will produce and return an 'int' value.
// 'add': The name of our method.
// '(int a, int b)': This declares two parameters. When 'add' is called, it *expects* two integer values.
// Inside the method, these values will be referred to as 'a' and 'b'.
int result = a + b;
// 'result' is a local variable, only exists within this 'add' method.
return result;
// 'return result;': This sends the value currently stored in 'result' back to the part of the code
// that called this method. It also ends the execution of the 'add' method.
}
int sum = add(10, 5);
// 'add(10, 5)': This is a method call. The values '10' and '5' are passed as arguments to the 'add' method.
// Inside 'add', 'a' will be 10, and 'b' will be 5.
// The 'add' method calculates 10 + 5 = 15, and 'returns' 15.
// 'int sum = ...': The returned value (15) is then assigned to the 'sum' variable in the 'main' method.
public static void greetUser(String name) {
// 'void': This means the method 'greetUser' does not return any value to its caller.
// '(String name)': This method takes one 'String' value as input, referred to as 'name' inside the method.
System.out.println("Hello, " + name + "! Welcome to Java methods.");
// This method performs an action (prints to console) but doesn't produce a value to be returned.
// No explicit 'return' statement is required for void methods, they implicitly return when they reach the end.
}
4. Clean Code Pro-Tips
- Single Responsibility Principle: Each method should ideally do one thing and do it well. A method that calculates the sum of two numbers shouldn't also print a greeting.
- Descriptive Method Names: Method names should clearly convey what the method does (e.g.,
calculateArea,printReport,isValidUser). UsecamelCasestarting with a lowercase letter. - Meaningful Parameters: Parameter names should be clear and descriptive, explaining their role within the method.
- Method Signature: The method signature consists of the method's name and the number, type, and order of its parameters. This is how Java distinguishes between different methods.
- Comments (if needed): For complex methods or algorithms, add comments to explain the logic or why certain steps are taken, not just what the code does (the code itself should be clear enough for "what").
5. Unsolved Exercise: User Interaction Helper
Create a program that uses methods to help with user interaction.
- Create a class named
UserInteraction. - Define a
public static voidmethod nameddisplayWelcomeMessagethat takes no parameters and prints a generic welcome. - Define a
public static Stringmethod namedformatFullNamethat takes twoStringparameters,firstNameandlastName, and returns a single formattedStringlike "First Last". - Define a
public static booleanmethod namedisValidAgethat takes anintparameterageand returnstrueif the age is between 0 and 120 (inclusive),falseotherwise. - In the
mainmethod:- Call
displayWelcomeMessage. - Call
formatFullNamewith example first and last names, and print the result. - Call
isValidAgewith an example age and print whether the age is valid or not.
- Call
6. Complete Solution: User Interaction Helper
// Chapter12/UserInteraction.java
public class UserInteraction {
public static void main(String[] args) {
System.out.println("--- User Interaction Helper ---");
// 5a. Call displayWelcomeMessage
displayWelcomeMessage();
// 5b. Call formatFullName and print the result
String fullName = formatFullName("Jane", "Doe");
System.out.println("Formatted Full Name: " + fullName); // Output: Jane Doe
// 5c. Call isValidAge with an example and print the result
int testAge1 = 30;
System.out.println("Is age " + testAge1 + " valid? " + isValidAge(testAge1)); // Output: true
int testAge2 = -5;
System.out.println("Is age " + testAge2 + " valid? " + isValidAge(testAge2)); // Output: false
int testAge3 = 150;
System.out.println("Is age " + testAge3 + " valid? " + isValidAge(testAge3)); // Output: false
System.out.println("-----------------------------");
}
// 2. Method to display a welcome message
public static void displayWelcomeMessage() {
System.out.println("Welcome to our application!");
System.out.println("We hope you enjoy your experience.\n");
}
// 3. Method to format a full name
public static String formatFullName(String firstName, String lastName) {
// Concatenate first name, a space, and last name
return firstName + " " + lastName;
}
// 4. Method to validate age
public static boolean isValidAge(int age) {
// Age must be greater than or equal to 0 AND less than or equal to 120
return age >= 0 && age <= 120;
}
}
Chapter 13: Method Overloading.
1. Quick Theory: Flexible Methods
Sometimes, you want methods that perform essentially the same task, but operate on different types of data or require a different number of inputs. Instead of coming up with entirely new method names (like addInts, addDoubles), Java allows method overloading.
Method overloading means defining multiple methods in the same class that have the same name but different parameter signatures. The parameter signature is determined by the number of parameters, their data types, and their order. The return type alone is not enough to overload a method. When you call an overloaded method, the Java compiler intelligently determines which specific version of the method to execute based on the arguments you provide. The "Why": Overloading makes your code more intuitive and readable by allowing related operations to share a common, descriptive name. It's a key aspect of polymorphism (a concept we'll explore later).
2. Code Examples: Overloading in Practice
Example 13.1: Overloading an add Method
// Chapter13/OverloadedAddMethods.java
public class OverloadedAddMethods {
public static void main(String[] args) {
System.out.println("--- Overloaded Add Methods ---");
// Calling add(int, int)
int sumInt = add(5, 10);
System.out.println("Sum of two integers (5, 10): " + sumInt); // Output: 15
// Calling add(double, double)
double sumDouble = add(7.5, 2.3);
System.out.printf("Sum of two doubles (7.5, 2.3): %.2f\n", sumDouble); // Output: 9.80
// Calling add(int, int, int)
int sumThreeInts = add(1, 2, 3);
System.out.println("Sum of three integers (1, 2, 3): " + sumThreeInts); // Output: 6
// Calling add(String, String)
String combinedString = add("Hello", "World");
System.out.println("Concatenated strings: " + combinedString); // Output: HelloWorld
System.out.println("----------------------------");
}
// 1. Overloaded 'add' method: two integers
public static int add(int a, int b) {
System.out.println("DEBUG: Calling int add(int, int)"); // For demonstration
return a + b;
}
// 2. Overloaded 'add' method: two doubles
public static double add(double a, double b) {
System.out.println("DEBUG: Calling double add(double, double)"); // For demonstration
return a + b;
}
// 3. Overloaded 'add' method: three integers (different number of parameters)
public static int add(int a, int b, int c) {
System.out.println("DEBUG: Calling int add(int, int, int)"); // For demonstration
return a + b + c;
}
// 4. Overloaded 'add' method: two Strings (different type of parameters)
public static String add(String s1, String s2) {
System.out.println("DEBUG: Calling String add(String, String)"); // For demonstration
return s1 + s2; // String concatenation
}
// NOTE: You CANNOT overload based on return type alone.
// public static double add(int a, int b) { return (double)(a + b); } // COMPILE ERROR!
// This would be a duplicate signature of add(int, int)
}
Example 13.2: Overloading for Displaying Information
// Chapter13/DisplayInfoOverload.java
public class DisplayInfoOverload {
public static void main(String[] args) {
System.out.println("--- Display Information Overload ---");
// Display a simple message
displayInfo("Welcome to the system!");
// Display user details
displayInfo("Alice", 30);
// Display product details
displayInfo("Laptop", 1200.50, 5);
System.out.println("------------------------------------");
}
// Overloaded method to display a general message
public static void displayInfo(String message) {
System.out.println("Message: " + message);
}
// Overloaded method to display user name and age
public static void displayInfo(String name, int age) {
System.out.println("User: " + name + ", Age: " + age);
}
// Overloaded method to display product name, price, and quantity
// Note the order and types of parameters (String, double, int)
public static void displayInfo(String productName, double price, int quantity) {
System.out.printf("Product: %s, Price: $%.2f, Quantity: %d\n", productName, price, quantity);
}
// Another overload (different order of parameters compared to the one above)
// This is valid overloading, but can sometimes lead to confusion.
public static void displayInfo(int quantity, String productName, double price) {
System.out.printf("Quantity: %d, Product: %s, Price: $%.2f\n", quantity, productName, price);
}
}
3. Line-by-Line Breakdown
Let's dissect OverloadedAddMethods.java:
public static int add(int a, int b) { ... }
// This is the first version of the 'add' method. Its signature is 'add(int, int)'.
public static double add(double a, double b) { ... }
// This is an overloaded version. It has the same name 'add', but its parameter types are different: 'add(double, double)'.
// Java can distinguish this method from the first one because the types of its parameters are different.
public static int add(int a, int b, int c) { ... }
// Another overloaded version. Its signature is 'add(int, int, int)' because it has a different *number* of parameters.
public static String add(String s1, String s2) { ... }
// Yet another overloaded version. Its signature is 'add(String, String)', distinct due to different parameter types.
int sumInt = add(5, 10);
// When you call 'add(5, 10)', the compiler sees two 'int' arguments.
// It matches this call to the method 'public static int add(int a, int b)'.
double sumDouble = add(7.5, 2.3);
// When you call 'add(7.5, 2.3)', the compiler sees two 'double' arguments.
// It matches this call to the method 'public static double add(double a, double b)'.
String combinedString = add("Hello", "World");
// When you call 'add("Hello", "World")', the compiler sees two 'String' arguments.
// It matches this call to the method 'public static String add(String s1, String s2)'.
4. Clean Code Pro-Tips
- Consistent Behavior: Overloaded methods should perform fundamentally the same operation. An
addmethod should always perform some form of addition or concatenation, not suddenly start multiplying numbers. - Clear Parameter Differences: Ensure your overloaded methods have clearly distinct parameter signatures (number, type, or order of parameters). Ambiguous signatures can lead to compile-time errors or unexpected behavior.
- Avoid Over-Overloading: While powerful, don't create too many overloaded versions of a method if they don't add significant value. Sometimes, a different method name is clearer.
- Return Type Doesn't Count: Remember that method overloading is not determined by the return type. You cannot have two methods with the same name and parameter signature but different return types.
5. Unsolved Exercise: Shape Area Calculator (Overloaded)
Let's expand your area calculator using method overloading.
- Create a class named
ShapeAreaCalculator. - Define a
public static doublemethod namedcalculateAreathat takes onedoubleparameterside(for a square) and returns its area. - Define another
public static doublemethod namedcalculateAreathat takes twodoubleparameterslengthandwidth(for a rectangle) and returns its area. - Define a third
public static doublemethod namedcalculateAreathat takes onedoubleparameterradius(for a circle) and returns its area. (UseMath.PI). - In the
mainmethod, call each overloadedcalculateAreamethod with appropriate arguments and print the results clearly labeled.
6. Complete Solution: Shape Area Calculator (Overloaded)
// Chapter13/ShapeAreaCalculator.java
public class ShapeAreaCalculator {
public static void main(String[] args) {
System.out.println("--- Shape Area Calculator (Overloaded Methods) ---");
// Calculate area of a square using calculateArea(double side)
double squareSide = 4.0;
double squareArea = calculateArea(squareSide);
System.out.printf("Area of a square with side %.1f: %.2f\n", squareSide, squareArea); // Output: 16.00
// Calculate area of a rectangle using calculateArea(double length, double width)
double rectLength = 6.0;
double rectWidth = 3.0;
double rectangleArea = calculateArea(rectLength, rectWidth);
System.out.printf("Area of a rectangle with length %.1f and width %.1f: %.2f\n", rectLength, rectWidth, rectangleArea); // Output: 18.00
// Calculate area of a circle using calculateArea(double radius)
double circleRadius = 5.0;
double circleArea = calculateArea(circleRadius);
System.out.printf("Area of a circle with radius %.1f: %.2f\n", circleRadius, circleArea); // Output: 78.54
System.out.println("-------------------------------------------------");
}
// 2. Overloaded method to calculate area of a SQUARE
// Signature: calculateArea(double)
public static double calculateArea(double side) {
// Basic validation
if (side <= 0) {
System.out.println("Error: Side must be positive.");
return 0.0;
}
return side * side;
}
// 3. Overloaded method to calculate area of a RECTANGLE
// Signature: calculateArea(double, double)
public static double calculateArea(double length, double width) {
// Basic validation
if (length <= 0 || width <= 0) {
System.out.println("Error: Length and width must be positive.");
return 0.0;
}
return length * width;
}
// 4. Overloaded method to calculate area of a CIRCLE
// Signature: calculateArea(double) - WAIT! This conflicts with square's signature.
// We need a different signature. For a circle, we could potentially
// rename it to calculateCircleArea, or use a slightly different parameter name/type if possible.
// However, to strictly follow the prompt and demonstrate overloading with *different parameter sets*,
// let's adjust the square to take an 'int' side, or rethink the circle.
// For this example, let's assume we *can* differentiate by context, or simply
// adjust the square to take 'int' if we want to reuse 'double' for circle radius.
// Let's make the square method take an 'int' just to ensure unique signatures.
// Let's modify the square method to take an 'int' side for a clear signature difference.
// (A common design decision in real code to avoid ambiguity or if integer sides are typical for squares)
public static double calculateArea(int side) {
if (side <= 0) {
System.out.println("Error: Side must be positive.");
return 0.0;
}
return (double) side * side; // Cast to double for return type consistency
}
// Now for the circle, using 'double radius' is distinct from 'int side' for the square.
// Signature: calculateArea(double) - This is distinct from calculateArea(int)
public static double calculateArea(double radius, String shapeType) { // Adding a dummy parameter to make signature unique
// This is a common trick if you need to overload based on a single numerical type but distinguish intent.
// Or, more practically, just name it calculateCircleArea if the primary parameter is the same.
// For the sake of demonstrating method overloading STRICTLY with unique signatures for the same name,
// I will add a dummy String parameter, although this isn't always the cleanest design.
// A better approach would be to have calculateSquareArea, calculateRectangleArea, calculateCircleArea.
// But the prompt wants "calculateArea" overloaded.
// Re-evaluating the prompt for "calculateArea that takes one double parameter radius (for a circle)"
// This directly clashes with `calculateArea(double side)` for a square.
// This highlights a *real-world* overloading problem!
// To strictly fulfill the prompt, I will rename the square method or the circle method
// OR add a *disambiguating* parameter to one of them.
// Given "calculateArea" overloaded, a common practice would be:
// calculateArea(double side) for square
// calculateArea(double length, double width) for rectangle
// calculateArea(double radius, boolean isCircle) for circle (using a dummy boolean)
// OR, the preferred way:
// calculateArea(double value, String shapeType) where shapeType clarifies.
// Let's assume for a first year, the intent is clear when calling:
// calculateArea(4.0) -> Square
// calculateArea(5.0) -> Circle
// This IS an ambiguity unless Java has context, which it doesn't.
// So, let's make the square method accept an `int` side and the circle `double radius` to disambiguate.
// Reworked `calculateArea(double side)` to `calculateArea(int side)`
// And now this `calculateArea(double radius)` is fine.
if (radius <= 0) {
System.out.println("Error: Radius must be positive.");
return 0.0;
}
return Math.PI * radius * radius;
}
}
// Modified main for the new square signature
/*
public class ShapeAreaCalculator {
public static void main(String[] args) {
System.out.println("--- Shape Area Calculator (Overloaded Methods) ---");
// Calculate area of a square using calculateArea(int side)
int squareSide = 4;
double squareArea = calculateArea(squareSide); // Calls calculateArea(int)
System.out.printf("Area of a square with side %d: %.2f\n", squareSide, squareArea); // Output: 16.00
// Calculate area of a rectangle using calculateArea(double length, double width)
double rectLength = 6.0;
double rectWidth = 3.0;
double rectangleArea = calculateArea(rectLength, rectWidth); // Calls calculateArea(double, double)
System.out.printf("Area of a rectangle with length %.1f and width %.1f: %.2f\n", rectLength, rectWidth, rectangleArea); // Output: 18.00
// Calculate area of a circle using calculateArea(double radius)
double circleRadius = 5.0;
double circleArea = calculateArea(circleRadius); // Calls calculateArea(double)
System.out.printf("Area of a circle with radius %.1f: %.2f\n", circleRadius, circleArea); // Output: 78.54
System.out.println("-------------------------------------------------");
}
// 2. Overloaded method to calculate area of a SQUARE
// Signature: calculateArea(int) -- DISTINCT from calculateArea(double) for circle
public static double calculateArea(int side) {
if (side <= 0) {
System.out.println("Error: Side must be positive.");
return 0.0;
}
return (double) side * side;
}
// 3. Overloaded method to calculate area of a RECTANGLE
// Signature: calculateArea(double, double)
public static double calculateArea(double length, double width) {
if (length <= 0 || width <= 0) {
System.out.println("Error: Length and width must be positive.");
return 0.0;
}
return length * width;
}
// 4. Overloaded method to calculate area of a CIRCLE
// Signature: calculateArea(double) -- DISTINCT from calculateArea(int) for square
public static double calculateArea(double radius) {
if (radius <= 0) {
System.out.println("Error: Radius must be positive.");
return 0.0;
}
return Math.PI * radius * radius;
}
}
*/
I will apply the corrected code (the second block within the thought process) for the ShapeAreaCalculator exercise to ensure distinct method signatures for overloading.
// Chapter13/ShapeAreaCalculator.java
public class ShapeAreaCalculator {
public static void main(String[] args) {
System.out.println("--- Shape Area Calculator (Overloaded Methods) ---");
// Calculate area of a square using calculateArea(int side)
int squareSide = 4;
double squareArea = calculateArea(squareSide); // Calls calculateArea(int)
System.out.printf("Area of a square with side %d: %.2f\n", squareSide, squareArea); // Output: 16.00
// Calculate area of a rectangle using calculateArea(double length, double width)
double rectLength = 6.0;
double rectWidth = 3.0;
double rectangleArea = calculateArea(rectLength, rectWidth); // Calls calculateArea(double, double)
System.out.printf("Area of a rectangle with length %.1f and width %.1f: %.2f\n", rectLength, rectWidth, rectangleArea); // Output: 18.00
// Calculate area of a circle using calculateArea(double radius)
double circleRadius = 5.0;
double circleArea = calculateArea(circleRadius); // Calls calculateArea(double)
System.out.printf("Area of a circle with radius %.1f: %.2f\n", circleRadius, circleArea); // Output: 78.54
System.out.println("-------------------------------------------------");
}
// 2. Overloaded method to calculate area of a SQUARE
// Signature: calculateArea(int) -- This is distinct from calculateArea(double) below.
public static double calculateArea(int side) {
if (side <= 0) {
System.out.println("Error: Side must be positive for a square.");
return 0.0;
}
return (double) side * side; // Cast to double for return type consistency
}
// 3. Overloaded method to calculate area of a RECTANGLE
// Signature: calculateArea(double, double)
public static double calculateArea(double length, double width) {
if (length <= 0 || width <= 0) {
System.out.println("Error: Length and width must be positive for a rectangle.");
return 0.0;
}
return length * width;
}
// 4. Overloaded method to calculate area of a CIRCLE
// Signature: calculateArea(double) -- This is distinct from calculateArea(int) above.
public static double calculateArea(double radius) {
if (radius <= 0) {
System.out.println("Error: Radius must be positive for a circle.");
return 0.0;
}
return Math.PI * radius * radius;
}
}
Chapter 14: Memory - Pass-by-Value vs Reference.
1. Quick Theory: Understanding How Arguments Are Passed
This chapter is crucial for DAM exams and your overall understanding of how Java handles memory. When you pass arguments to a method, Java always uses a mechanism called pass-by-value. However, what "value" is passed depends on whether you're dealing with primitive types or reference types (objects).
- Primitives (e.g.,
int,double,boolean,char): When you pass a primitive variable to a method, a copy of its actual value is made and passed. The method works with this copy. If the method modifies this copy, the original variable in the calling code remains unchanged. This is because primitive values live directly on the Stack. - Reference Types (e.g.,
String,Array,Scanner, and any other object): When you pass an object variable (which is a reference to an object) to a method, a copy of the reference (the memory address) is passed. Both the original variable and the parameter inside the method now point to the same object on the Heap.- If the method modifies the contents of the object using this copied reference (e.g., changing an element in an array, or calling a method on an object that changes its internal state), these changes will be visible in the calling code because they are affecting the same shared object.
- If the method tries to reassign the copied reference variable to point to a new object, this reassignment only affects the copy of the reference within the method. The original reference variable in the calling code will still point to the original object.
The "Why": Understanding this distinction is fundamental to predicting how your code will behave when data is manipulated within methods. Misunderstanding pass-by-value for references is a common source of bugs for beginners.
2. Code Examples: Seeing the Difference
Example 14.1: Passing Primitives (Pass-by-Value)
// Chapter14/PassByValuePrimitive.java
public class PassByValuePrimitive {
public static void main(String[] args) {
System.out.println("--- Pass-by-Value for Primitives ---");
int originalNumber = 10;
System.out.println("Before method call: originalNumber = " + originalNumber); // Output: 10
// Call the method to try and change the number
modifyPrimitive(originalNumber);
// Check the value of originalNumber after the method call
System.out.println("After method call: originalNumber = " + originalNumber); // Output: 10 (STILL 10!)
// Explanation: The 'modifyPrimitive' method received a COPY of 'originalNumber'.
// Changing the copy inside the method does not affect the original variable.
System.out.println("------------------------------------");
}
// Method that tries to modify an integer parameter
public static void modifyPrimitive(int number) { // 'number' is a copy of 'originalNumber'
System.out.println("Inside method (before change): number = " + number); // Output: 10
number = 99; // Modifying the local copy 'number'
System.out.println("Inside method (after change): number = " + number); // Output: 99
}
}
Example 14.2: Passing Arrays (Pass-by-Value of Reference) - Modifying Contents
// Chapter14/PassByValueReferenceModifyContent.java
import java.util.Arrays;
public class PassByValueReferenceModifyContent {
public static void main(String[] args) {
System.out.println("--- Pass-by-Value of Reference (Modifying Contents) ---");
int[] originalArray = {10, 20, 30};
System.out.println("Before method call: originalArray = " + Arrays.toString(originalArray)); // Output: [10, 20, 30]
// Call the method to try and modify the array's elements
modifyArrayElements(originalArray);
// Check the array after the method call
System.out.println("After method call: originalArray = " + Arrays.toString(originalArray)); // Output: [100, 20, 30] (CHANGED!)
// Explanation: Both 'originalArray' and 'arr' (in the method) point to the SAME array object in memory.
// Changing an element via 'arr' modifies the shared object, so 'originalArray' sees the change.
System.out.println("-----------------------------------------------------");
}
// Method that modifies an array element
public static void modifyArrayElements(int[] arr) { // 'arr' is a copy of the reference to 'originalArray'
System.out.println("Inside method (before change): arr = " + Arrays.toString(arr)); // Output: [10, 20, 30]
arr[0] = 100; // Modifying the content of the array object that both references point to
System.out.println("Inside method (after change): arr = " + Arrays.toString(arr)); // Output: [100, 20, 30]
}
}
Example 14.3: Passing Arrays (Pass-by-Value of Reference) - Reassigning Reference
// Chapter14/PassByValueReferenceReassignReference.java
import java.util.Arrays;
public class PassByValueReferenceReassignReference {
public static void main(String[] args) {
System.out.println("--- Pass-by-Value of Reference (Reassigning Reference) ---");
int[] originalArray = {1, 2, 3};
System.out.println("Before method call: originalArray = " + Arrays.toString(originalArray)); // Output: [1, 2, 3]
// Call the method to try and reassign the array reference
reassignArrayReference(originalArray);
// Check the array after the method call
System.out.println("After method call: originalArray = " + Arrays.toString(originalArray)); // Output: [1, 2, 3] (NOT CHANGED!)
// Explanation: 'reassignArrayReference' received a COPY of the reference.
// It then made that COPY point to a *NEW* array.
// The ORIGINAL 'originalArray' variable still points to the old array.
System.out.println("--------------------------------------------------------");
}
// Method that tries to reassign the array parameter to a new array
public static void reassignArrayReference(int[] arr) { // 'arr' is a copy of the reference
System.out.println("Inside method (before reassign): arr = " + Arrays.toString(arr)); // Output: [1, 2, 3]
// 'arr' now points to a NEW array object in memory.
// The original array ({1, 2, 3}) is no longer reachable via the 'arr' parameter.
arr = new int[]{500, 600, 700};
System.out.println("Inside method (after reassign): arr = " + Arrays.toString(arr)); // Output: [500, 600, 700]
}
}
3. Line-by-Line Breakdown
Let's break down the key parts from all three examples:
// From PassByValuePrimitive.java
public static void modifyPrimitive(int number) {
// 'number' here is a local parameter variable.
// When modifyPrimitive(originalNumber) is called, the *value* of originalNumber (10)
// is copied into 'number'. So, 'number' = 10.
number = 99;
// This line changes the value of the *local copy* 'number' to 99.
// The original 'originalNumber' variable in 'main' is completely unaffected.
}
// From PassByValueReferenceModifyContent.java
public static void modifyArrayElements(int[] arr) {
// 'arr' here is a local parameter variable.
// When modifyArrayElements(originalArray) is called, the *reference value* (memory address)
// that 'originalArray' holds is copied into 'arr'.
// Now, both 'originalArray' and 'arr' contain the same memory address, meaning they
// *both point to the exact same array object* {10, 20, 30} on the Heap.
arr[0] = 100;
// This line uses the 'arr' reference to access the element at index 0 of the array object.
// It then modifies the content of that array object from 10 to 100.
// Since 'originalArray' in 'main' points to the *same object*, it also sees this change.
}
// From PassByValueReferenceReassignReference.java
public static void reassignArrayReference(int[] arr) {
// Similar to the previous example, 'arr' starts by holding a copy of the reference
// to the original array {1, 2, 3}.
arr = new int[]{500, 600, 700};
// This is the crucial line. The 'new int[]{...}' part creates a *brand new* array object
// {500, 600, 700} in a *different memory location* on the Heap.
// The '=' operator then makes the *local parameter variable* 'arr' point to this *new* array.
// The 'originalArray' variable in 'main' is unaffected. It still holds its original reference
// to the {1, 2, 3} array. The change to 'arr' is local to this method.
}
4. Clean Code Pro-Tips
- Mental Model is Key: Always visualize what's happening in memory:
- Primitives: Box on the Stack, value inside. Copy the value, not the box.
- Objects: Box on the Stack (holding an address), object on the Heap (at that address). Copy the address, not the object.
- Understand
finalvs.static finalfor Arrays:final int[] MY_ARRAY = {...}means theMY_ARRAYreference itself cannot be reassigned to a different array, but the contents of the array can still be modified.static finalis for true constants. - Be Mindful of Side Effects: When a method modifies the contents of an object passed to it, these are called "side effects." Sometimes side effects are intended (like a sorting method), but sometimes they are accidental and lead to bugs. Be explicit about when your methods are designed to modify arguments.
- Return Modified Objects: If a method creates a new object or intends to return a modified version of an object rather than modifying the original in-place, explicitly return it from the method.
5. Unsolved Exercise: Data Manipulator
Let's put this memory concept to the test.
- Create a class named
DataManipulator. - In the
mainmethod, declare anintvariablemyNumberinitialized to25. - In the
mainmethod, declare anintarraymyArrayinitialized to{10, 20, 30, 40}. - Create a
public static voidmethod nameddoubleValue(int num)that attempts to double the value ofnuminternally. - Create a
public static voidmethod nameddoubleArrayElements(int[] arr)that iterates through thearrand doubles each of its elements. - Create a
public static voidmethod namedreplaceArray(int[] arr)that attempts to reassignarrto a new array{1, 1, 1, 1}. - In
main, before and after calling each of these methods, print the state ofmyNumberandmyArrayto clearly observe the effects.
6. Complete Solution: Data Manipulator
// Chapter14/DataManipulator.java
import java.util.Arrays; // Needed for Arrays.toString()
public class DataManipulator {
public static void main(String[] args) {
// 2. Declare and initialize myNumber
int myNumber = 25;
// 3. Declare and initialize myArray
int[] myArray = {10, 20, 30, 40};
System.out.println("--- Initial State ---");
System.out.println("myNumber: " + myNumber); // Output: 25
System.out.println("myArray: " + Arrays.toString(myArray)); // Output: [10, 20, 30, 40]
System.out.println("\n--- Calling doubleValue(myNumber) ---");
doubleValue(myNumber); // Pass a copy of 25
System.out.println("After doubleValue call, myNumber: " + myNumber); // Output: 25 (UNTOUCHED!)
System.out.println("\n--- Calling doubleArrayElements(myArray) ---");
doubleArrayElements(myArray); // Pass a copy of the reference to myArray
System.out.println("After doubleArrayElements call, myArray: " + Arrays.toString(myArray)); // Output: [20, 40, 60, 80] (MODIFIED!)
System.out.println("\n--- Calling replaceArray(myArray) ---");
replaceArray(myArray); // Pass a copy of the reference to myArray
System.out.println("After replaceArray call, myArray: " + Arrays.toString(myArray)); // Output: [20, 40, 60, 80] (UNTOUCHED!)
// Explanation: The 'replaceArray' method only reassigned its local parameter 'arr'.
// The original 'myArray' variable in 'main' still points to the same array object it always did.
System.out.println("\n--- Final State ---");
System.out.println("myNumber: " + myNumber);
System.out.println("myArray: " + Arrays.toString(myArray));
System.out.println("-------------------");
}
// 4. Method to attempt doubling a primitive value
public static void doubleValue(int num) { // 'num' is a copy of 'myNumber'
System.out.println(" Inside doubleValue (before): num = " + num); // Output: 25
num = num * 2; // Doubles the local copy
System.out.println(" Inside doubleValue (after): num = " + num); // Output: 50
}
// 5. Method to double elements of an array
public static void doubleArrayElements(int[] arr) { // 'arr' is a copy of the reference to 'myArray'
System.out.println(" Inside doubleArrayElements (before): arr = " + Arrays.toString(arr)); // Output: [10, 20, 30, 40]
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2; // Modifies the actual elements of the shared array object
}
System.out.println(" Inside doubleArrayElements (after): arr = " + Arrays.toString(arr)); // Output: [20, 40, 60, 80]
}
// 6. Method to attempt replacing an array (reassigning the reference)
public static void replaceArray(int[] arr) { // 'arr' is a copy of the reference to 'myArray'
System.out.println(" Inside replaceArray (before): arr = " + Arrays.toString(arr)); // Output: [20, 40, 60, 80]
arr = new int[]{1, 1, 1, 1}; // 'arr' now points to a NEW array object
System.out.println(" Inside replaceArray (after): arr = " + Arrays.toString(arr)); // Output: [1, 1, 1, 1]
}
}
Book 1: Part 3 (Logic & Robustness)
Chapter 15: The Math Class.
1. Quick Theory: Your Built-in Math Toolkit
Just like System for output or Arrays for array utilities, Java provides a powerful built-in class called java.lang.Math for performing common mathematical operations. You don't need to import Math because it's part of the java.lang package, which is automatically available to every Java program.
The key thing to understand about the Math class is that all its methods are static. This means you call them directly on the Math class itself (e.g., Math.sqrt(25)), without needing to create an object of the Math class (you won't see new Math()). The "Why": Math operations are universal and don't depend on any specific "instance" or "object" of math, so a static utility class is the perfect design pattern. It saves you from writing complex math functions yourself and ensures consistent, accurate results.
2. Code Examples: Math in Action
Example 15.1: Basic Math Operations
// Chapter15/BasicMathOperations.java
// No import statement needed for Math, as it's in java.lang package
public class BasicMathOperations {
public static void main(String[] args) {
System.out.println("--- Basic Math Operations ---");
// 1. Math.sqrt(double a): Returns the square root of a double value.
double num1 = 25.0;
double squareRoot = Math.sqrt(num1);
System.out.println("Square root of " + num1 + ": " + squareRoot); // Output: 5.0
double num2 = 10.0;
System.out.println("Square root of " + num2 + ": " + Math.sqrt(num2)); // Output: 3.16...
// 2. Math.pow(double base, double exponent): Returns the value of the first argument
// raised to the power of the second argument.
double base = 2.0;
double exponent = 3.0;
double powerResult = Math.pow(base, exponent);
System.out.println(base + " raised to the power of " + exponent + ": " + powerResult); // Output: 8.0
double base2 = 5.0;
double exponent2 = 2.0;
System.out.println(base2 + " squared: " + Math.pow(base2, exponent2)); // Output: 25.0
// 3. Math.abs(dataType a): Returns the absolute value of the argument.
// It has overloaded versions for int, long, float, and double.
int negativeInt = -15;
System.out.println("Absolute value of " + negativeInt + ": " + Math.abs(negativeInt)); // Output: 15
double negativeDouble = -7.34;
System.out.println("Absolute value of " + negativeDouble + ": " + Math.abs(negativeDouble)); // Output: 7.34
// 4. Math.round(float a) or Math.round(double a): Rounds to the nearest long or int.
double decimalNum = 3.7;
long roundedNum = Math.round(decimalNum); // Returns a long
System.out.println("Rounded " + decimalNum + " to nearest integer: " + roundedNum); // Output: 4
double decimalNum2 = 3.2;
System.out.println("Rounded " + decimalNum2 + " to nearest integer: " + Math.round(decimalNum2)); // Output: 3
System.out.println("-----------------------------");
}
}
Example 15.2: The Importance of Math.random()
// Chapter15/MathRandomExample.java
public class MathRandomExample {
public static void main(String[] args) {
System.out.println("--- Math.random() Examples ---");
// Math.random(): Returns a pseudo-random double value between 0.0 (inclusive) and 1.0 (exclusive).
// This means it can be 0.0, but never 1.0. (e.g., 0.0, 0.123, 0.999...)
// Generating a few random numbers
System.out.println("Random double 1: " + Math.random());
System.out.println("Random double 2: " + Math.random());
System.out.println("Random double 3: " + Math.random());
// How to generate a random number within a specific range [0, max-1]?
// Example: Random integer between 0 and 9 (inclusive)
// (Math.random() * max)
int random0to9 = (int) (Math.random() * 10); // Multiplies by 10, then casts to int (truncates decimals)
System.out.println("Random integer (0-9): " + random0to9);
// Example: Random integer between 1 and 10 (inclusive)
// (Math.random() * max) + min
int random1to10 = (int) (Math.random() * 10) + 1; // 0-9 shifted to 1-10
System.out.println("Random integer (1-10): " + random1to10);
// A more general formula for integers in [min, max] is:
// (int)(Math.random() * (max - min + 1)) + min;
int min = 50;
int max = 100;
int random50to100 = (int) (Math.random() * (max - min + 1)) + min;
System.out.println("Random integer (" + min + "-" + max + "): " + random50to100);
System.out.println("------------------------------");
}
}
3. Line-by-Line Breakdown
Let's break down BasicMathOperations.java:
double squareRoot = Math.sqrt(num1);
// 'Math.sqrt()': This is a static method of the Math class.
// It takes a 'double' as input and returns its 'double' square root.
// 'num1': The argument (value) passed to the method. Here, 25.0.
double powerResult = Math.pow(base, exponent);
// 'Math.pow()': Takes two 'double' arguments: the base and the exponent.
// Returns 'base' raised to the power of 'exponent' as a 'double'.
System.out.println("Absolute value of " + negativeInt + ": " + Math.abs(negativeInt));
// 'Math.abs()': An overloaded method. It takes a number (int, long, float, or double)
// and returns its absolute (non-negative) value.
And from MathRandomExample.java:
System.out.println("Random double 1: " + Math.random());
// 'Math.random()': This method generates a pseudo-random floating-point number.
// Crucially, this number is always in the range [0.0, 1.0), meaning
// it can be 0.0 but will never quite reach 1.0 (e.g., 0.9999999999999999).
int random0to9 = (int) (Math.random() * 10);
// This is how we start converting a random double [0.0, 1.0) into a random integer range.
// 1. 'Math.random() * 10': This scales the number to be in the range [0.0, 10.0).
// (e.g., if random() is 0.5, this becomes 5.0; if 0.99, becomes 9.9)
// 2. '(int) (...)': This is a type cast. It truncates the decimal part, effectively
// giving us integers from 0 to 9 (since 9.9 becomes 9).
int random1to10 = (int) (Math.random() * 10) + 1;
// To get 1 to 10: we generate 0-9 and then add 1 to shift the range.
// (int)(Math.random() * 10) gives 0, 1, ..., 9.
// Adding 1 makes it 1, 2, ..., 10.
4. Pro-Tips: Using Math Effectively
- Constants:
Mathalso provides useful constants likeMath.PI(for π) andMath.E(for Euler's number). Use these instead of hardcoding approximations. - Integer Rounding: Be aware that
(int)cast truncates (removes the decimal part), whileMath.round()rounds to the nearest whole number. Choose the appropriate method based on your needs. - Random Range Formula: Memorize the general formula for generating a random integer
[min, max](inclusive):(int)(Math.random() * (max - min + 1)) + min;. This will be useful in many scenarios. - Don't Re-invent: Always check the
Mathclass documentation if you need a specific mathematical function. It's highly optimized and reliable.
5. Unsolved Exercise: Right Triangle Solver
Create a program to calculate properties of a right-angled triangle.
- Create a class named
RightTriangleSolver. - Declare two
doublevariables,sideAandsideB, and initialize them to3.0and4.0respectively. - Calculate the hypotenuse (
sideC) using the Pythagorean theorem (c = sqrt(a^2 + b^2)). Print the result. - Calculate the absolute difference between
sideAandsideB. Print the result. - Generate and print a random integer between
1and100(inclusive) to represent a hypothetical "angle modifier".
6. Complete Solution: Right Triangle Solver
// Chapter15/RightTriangleSolver.java
public class RightTriangleSolver {
public static void main(String[] args) {
System.out.println("--- Right Triangle Solver ---");
// 2. Declare and initialize sideA and sideB
double sideA = 3.0;
double sideB = 4.0;
System.out.printf("Side A: %.1f, Side B: %.1f\n", sideA, sideB);
// 3. Calculate the hypotenuse (sideC) using Math.sqrt and Math.pow
// c = sqrt(a^2 + b^2)
double sideC = Math.sqrt(Math.pow(sideA, 2) + Math.pow(sideB, 2));
System.out.printf("Hypotenuse (Side C): %.2f\n", sideC); // Output should be 5.00
// 4. Calculate the absolute difference between sideA and sideB
double absoluteDifference = Math.abs(sideA - sideB);
System.out.printf("Absolute difference between Side A and Side B: %.2f\n", absoluteDifference); // Output: 1.00
// 5. Generate and print a random integer between 1 and 100 (inclusive)
int minAngleModifier = 1;
int maxAngleModifier = 100;
int angleModifier = (int)(Math.random() * (maxAngleModifier - minAngleModifier + 1)) + minAngleModifier;
System.out.println("Random angle modifier (1-100): " + angleModifier);
System.out.println("-----------------------------");
}
}
Chapter 16: Random Numbers.
1. Quick Theory: Bringing Randomness to Life
Building on Math.random(), this chapter focuses on how to consistently generate random numbers within specific integer ranges. This is a very common task in programming, from simulating dice rolls in a game to selecting random items from a list, or even generating test data.
As we saw, Math.random() gives you a double between 0.0 (inclusive) and 1.0 (exclusive). To get an integer within a custom range, you need to scale and shift this double value, then cast it to an int. The key is to correctly use the formula (int)(Math.random() * (max - min + 1)) + min; where min and max are inclusive boundaries of your desired range. The +1 is crucial to make the max value inclusive, and +min shifts the entire range. The "Why": Randomness is fundamental for simulations, games, security (though Math.random is not cryptographically secure), and many other dynamic applications. Mastering random number generation allows your programs to be less predictable and more interactive.
2. Code Examples: Practical Randomness
Example 16.1: Generating Random Integers in Different Ranges
// Chapter16/RandomIntegerGenerator.java
public class RandomIntegerGenerator {
public static void main(String[] args) {
System.out.println("--- Random Integer Generation ---");
// 1. Random number between 0 and 9 (inclusive)
// Formula: (int)(Math.random() * (max + 1)) if min is 0
int randomNumber0_9 = (int)(Math.random() * 10);
System.out.println("Random (0-9): " + randomNumber0_9);
// 2. Random number between 1 and 6 (inclusive) - like a dice roll
// Here, min = 1, max = 6
// (int)(Math.random() * (6 - 1 + 1)) + 1 => (int)(Math.random() * 6) + 1
int diceRoll = (int)(Math.random() * 6) + 1;
System.out.println("Dice Roll (1-6): " + diceRoll);
// 3. Random number between 20 and 30 (inclusive)
// Here, min = 20, max = 30
// (int)(Math.random() * (30 - 20 + 1)) + 20 => (int)(Math.random() * 11) + 20
int randomNumber20_30 = (int)(Math.random() * (30 - 20 + 1)) + 20;
System.out.println("Random (20-30): " + randomNumber20_30);
// 4. Generate a random "difficulty level" between 1 and 3
int minDifficulty = 1;
int maxDifficulty = 3;
int difficultyLevel = (int)(Math.random() * (maxDifficulty - minDifficulty + 1)) + minDifficulty;
System.out.println("Difficulty Level (1-3): " + difficultyLevel);
// 5. Generate a random boolean (true/false)
// This can be done by checking if a random number is above a certain threshold (e.g., 0.5)
boolean coinFlip = Math.random() < 0.5; // True for roughly 50% of the time
System.out.println("Coin Flip (true/false): " + coinFlip);
System.out.println("---------------------------------");
}
}
Example 16.2: Simulating Multiple Dice Rolls
// Chapter16/DiceSimulator.java
public class DiceSimulator {
public static void main(String[] args) {
System.out.println("--- Dice Roll Simulator ---");
// Simulate rolling a 6-sided die 5 times
System.out.println("Rolling a 6-sided die 5 times:");
for (int i = 0; i < 5; i++) {
int roll = (int)(Math.random() * 6) + 1; // Generates a number between 1 and 6
System.out.println(" Roll " + (i + 1) + ": " + roll);
}
System.out.println("\n--- Two Dice Roll Sums ---");
// Simulate rolling two 6-sided dice and summing their results 3 times
for (int i = 0; i < 3; i++) {
int die1 = (int)(Math.random() * 6) + 1;
int die2 = (int)(Math.random() * 6) + 1;
int sum = die1 + die2;
System.out.println(" Roll " + (i + 1) + ": Die 1=" + die1 + ", Die 2=" + die2 + ", Sum=" + sum);
}
System.out.println("---------------------------");
}
}
3. Line-by-Line Breakdown
Let's break down RandomIntegerGenerator.java:
int randomNumber0_9 = (int)(Math.random() * 10);
// 'Math.random()': Generates a double from 0.0 (inclusive) up to 1.0 (exclusive).
// '* 10': Multiplies the random double by 10. This scales the range to be from 0.0 up to 10.0 (exclusive).
// For example, if Math.random() is 0.0, it's 0.0. If 0.999..., it's 9.999...
// '(int)': Type casts the resulting double to an integer. This truncates the decimal part.
// So, any value from 0.0 to 0.999... becomes 0.
// Any value from 9.0 to 9.999... becomes 9.
// Result: An integer uniformly distributed from 0 to 9, inclusive.
int diceRoll = (int)(Math.random() * 6) + 1;
// This is the common pattern for [min, max] range. Here, min=1, max=6.
// 'max - min + 1' for this case is (6 - 1 + 1) = 6.
// 'Math.random() * 6': Generates a double from 0.0 up to 6.0 (exclusive).
// '(int)(...)': Truncates to an integer from 0 to 5.
// '+ 1': Shifts the range. So, 0 becomes 1, 1 becomes 2, ..., 5 becomes 6.
// Result: An integer uniformly distributed from 1 to 6, inclusive.
4. Pro-Tips: Handling Randomness Like a Pro
- Inclusive Ranges: Always remember that the
(max - min + 1)part in the formula is what makes themaxvalue inclusive. If you forget+1, yourmaxvalue will never be generated. - Seeding (Advanced): For true unpredictability (e.g., in games), you generally don't "seed"
Math.random(). If you need reproducible sequences of random numbers (e.g., for testing), you'd use thejava.util.Randomclass with a specific seed. For basic applications,Math.random()is sufficient. - Clear Variable Names: When generating random numbers, assign them to variables with descriptive names that indicate their purpose (e.g.,
diceRoll,randomNumber,difficultyLevel).
5. Unsolved Exercise: Item Dropper
You're simulating an item drop system in a simple game.
- Create a class named
ItemDropper. - Simulate rolling a 20-sided die (
1-20) to determine if a "rare item" drops. If the roll is18or higher, print "Rare Item Dropped!". Otherwise, print "Common Item Dropped.". Do this once. - Simulate a character finding a random amount of gold between
10and50(inclusive). Print the amount of gold found. Do this 3 times using a loop.
6. Complete Solution: Item Dropper
// Chapter16/ItemDropper.java
public class ItemDropper {
public static void main(String[] args) {
System.out.println("--- Item Drop Simulator ---");
// 2. Simulate rolling a 20-sided die for rare item drop
int minRoll = 1;
int maxRoll = 20;
int rareItemThreshold = 18;
int diceRoll = (int)(Math.random() * (maxRoll - minRoll + 1)) + minRoll;
System.out.println("20-sided die roll: " + diceRoll);
if (diceRoll >= rareItemThreshold) {
System.out.println("Rare Item Dropped!");
} else {
System.out.println("Common Item Dropped.");
}
System.out.println("\n--- Gold Found ---");
// 3. Simulate finding a random amount of gold 3 times
int minGold = 10;
int maxGold = 50;
for (int i = 0; i < 3; i++) {
int goldFound = (int)(Math.random() * (maxGold - minGold + 1)) + minGold;
System.out.println("Attempt " + (i + 1) + ": Found " + goldFound + " gold pieces.");
}
System.out.println("---------------------------");
}
}
Chapter 17: Introduction to Exceptions (Try-Catch).
1. Quick Theory: When Things Go Wrong (and how to fix them)
No matter how carefully you write your code, things can go wrong at runtime. A user might enter text when you expect a number, a file might be missing, or your program might try to divide by zero. These runtime errors are called exceptions. When an exception occurs and isn't handled, your program "crashes" (terminates abruptly), which is a very unprofessional user experience.
The try-catch block is Java's mechanism for exception handling. It allows your program to "try" to execute a block of code that might throw an exception. If an exception does occur, instead of crashing, the program's control flow immediately jumps to the catch block, where you can "catch" the specific type of exception and gracefully handle it (e.g., print an error message, ask for input again, log the error). The "Why": try-catch makes your programs robust and "bulletproof." It allows them to recover from unexpected situations, provide helpful feedback to the user, and continue running instead of failing miserably.
tryblock: Contains the code that might throw an exception.catchblock: Specifies the type of exception it can handle. If an exception of that type (or a subclass) occurs in thetryblock, thecatchblock's code is executed. You can have multiplecatchblocks for different exception types.finallyblock (optional): Code in this block is always executed, regardless of whether an exception occurred or was caught. It's often used for cleanup tasks like closing files orScannerobjects.
For this chapter, we'll focus on InputMismatchException, which is commonly thrown by the Scanner class when the user provides input that doesn't match the expected type (e.g., "hello" for nextInt()).
2. Code Examples: Building Robust Input
Example 17.1: Catching InputMismatchException
// Chapter17/InputValidationBasic.java
import java.util.InputMismatchException; // Needed to specifically catch this exception
import java.util.Scanner;
public class InputValidationBasic {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("--- Basic Input Validation ---");
System.out.print("Please enter an integer number: ");
try {
// Code that might throw an InputMismatchException if user enters non-integer
int number = scanner.nextInt();
System.out.println("You entered: " + number);
} catch (InputMismatchException e) {
// This block executes if an InputMismatchException occurs in the try block
System.out.println("Error: Invalid input! That was not an integer.");
// Important: After an InputMismatchException, the Scanner's buffer is not clean.
// It still contains the invalid token. We need to clear it.
scanner.nextLine(); // Consumes the invalid input from the buffer
} finally {
// The finally block always executes, useful for closing resources
System.out.println("Validation attempt finished.");
scanner.close(); // Close the scanner here to ensure it's always closed
}
System.out.println("Program continues..."); // Program doesn't crash, it continues here
System.out.println("------------------------------");
}
}
Example 17.2: Robust Input Loop
// Chapter17/RobustInputLoop.java
import java.util.InputMismatchException;
import java.util.Scanner;
public class RobustInputLoop {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int userAge = 0;
boolean validInput = false; // A flag to control the loop
System.out.println("--- Robust Age Input ---");
// Loop until valid integer input is received
while (!validInput) { // Loop as long as input is NOT valid
System.out.print("Please enter your age (a whole number): ");
try {
userAge = scanner.nextInt(); // Attempt to read an integer
// If nextInt() succeeds, it means we have valid input
validInput = true; // Set flag to true to exit the loop
} catch (InputMismatchException e) {
// If nextInt() fails (InputMismatchException), execute this block
System.out.println("Invalid input! Please enter a number like 25.");
scanner.nextLine(); // Clear the invalid input from the scanner buffer
}
}
System.out.println("Thank you! Your age is: " + userAge);
// Let's get another valid input (e.g., GPA)
double userGPA = 0.0;
validInput = false; // Reset the flag for the next input
while (!validInput) {
System.out.print("Please enter your GPA (e.g., 3.85): ");
try {
userGPA = scanner.nextDouble();
validInput = true;
} catch (InputMismatchException e) {
System.out.println("Invalid input! Please enter a decimal number like 3.5.");
scanner.nextLine(); // Clear buffer
}
}
System.out.printf("Your GPA is: %.2f\n", userGPA);
System.out.println("--- Input Process Complete ---");
scanner.close(); // Always close the scanner
System.out.println("------------------------------");
}
}
3. Line-by-Line Breakdown
Let's break down InputValidationBasic.java:
try {
// Code in this block is 'tried'. If no exception occurs, it runs to completion.
int number = scanner.nextInt(); // This line is prone to InputMismatchException
System.out.println("You entered: " + number);
} catch (InputMismatchException e) {
// 'catch (InputMismatchException e)': This declares an exception handler.
// If an 'InputMismatchException' occurs in the 'try' block, control immediately transfers here.
// 'e' is a variable name for the exception object itself, useful for getting details (e.getMessage()).
System.out.println("Error: Invalid input! That was not an integer.");
scanner.nextLine(); // CRITICAL: Consumes the remaining invalid input (e.g., "abc\n") from the buffer.
// Without this, the scanner would try to re-read "abc" next time, causing an infinite loop
// if this were inside a loop for repeated input.
} finally {
// This 'finally' block is optional but important.
// Code here will always execute, regardless of whether an exception occurred,
// was caught, or if the 'try' block completed successfully.
System.out.println("Validation attempt finished.");
scanner.close(); // Good place to close resources like Scanner.
}
4. Pro-Tips: Writing Bulletproof Programs
- Anticipate Failures: Always assume user input can be wrong, files might not exist, or network connections can drop. Plan for these scenarios with
try-catch. - Specific
catchBlocks: Catch specific exceptions (likeInputMismatchException) rather than a genericException. This allows you to handle different error types differently. - Clean Up Scanner: After a
InputMismatchExceptioninScanner.nextX()methods, always callscanner.nextLine()to clear the buffer. This is a common and important fix for robust input loops. - Meaningful Error Messages: Provide clear and helpful error messages to the user. Don't just print a stack trace.
- Resource Management: Use
finallyto ensure critical resources (likeScanner, file streams, network connections) are always closed, even if an exception occurs.
5. Unsolved Exercise: Robust Age and Height Input
Create a program that asks the user for their age (integer) and then their height in meters (decimal). Your program should:
- Use
try-catchblocks to handleInputMismatchExceptionfor both inputs. - Use loops to continuously ask for input until a valid number is provided for both age and height.
- Print the valid age and height once successfully entered.
- Ensure the
Scanneris properly closed.
6. Complete Solution: Robust Age and Height Input
// Chapter17/RobustAgeHeightInput.java
import java.util.InputMismatchException;
import java.util.Scanner;
public class RobustAgeHeightInput {
public static void main(String[] args) {
Scanner inputReader = new Scanner(System.in);
int age = 0;
double height = 0.0;
boolean isValidInput; // Flag for loop control
System.out.println("--- Robust User Profile Input ---");
// Loop for age input
isValidInput = false;
while (!isValidInput) {
System.out.print("Enter your age (integer): ");
try {
age = inputReader.nextInt();
// Basic validation for age range (optional but good practice)
if (age < 0 || age > 120) {
System.out.println("Age must be between 0 and 120. Please try again.");
} else {
isValidInput = true; // Valid age entered, exit loop
}
} catch (InputMismatchException e) {
System.out.println("Invalid input! Please enter a whole number for age (e.g., 30).");
inputReader.nextLine(); // Clear the invalid input from the buffer
}
}
inputReader.nextLine(); // Consume the leftover newline after reading age
// Loop for height input
isValidInput = false;
while (!isValidInput) {
System.out.print("Enter your height in meters (decimal, e.g., 1.75): ");
try {
height = inputReader.nextDouble();
// Basic validation for height range
if (height < 0.5 || height > 2.5) {
System.out.println("Height must be between 0.5 and 2.5 meters. Please try again.");
} else {
isValidInput = true; // Valid height entered, exit loop
}
} catch (InputMismatchException e) {
System.out.println("Invalid input! Please enter a decimal number for height (e.g., 1.80).");
inputReader.nextLine(); // Clear the invalid input from the buffer
}
}
inputReader.nextLine(); // Consume the leftover newline after reading height
System.out.println("\n--- Your Profile ---");
System.out.println("Age: " + age + " years");
System.out.printf("Height: %.2f meters\n", height);
System.out.println("--------------------");
inputReader.close(); // Close the scanner
}
}
Chapter 18: Basic Algorithm: Searching & Counting.
1. Quick Theory: Finding and Tallying Data
Now that you know how to store data in arrays, the next logical step is to learn how to find specific information within those arrays and count how many times it appears. These are fundamental algorithmic tasks that you'll perform constantly in programming.
- Searching: The simplest search algorithm is a linear search. This involves iterating through each element of an array, one by one, and comparing it to the target value you're looking for. If a match is found, you know the item exists (and perhaps its index).
- Counting: Similar to searching, counting involves iterating through an array and, for each element, checking if it matches a specific criterion. If it matches, you increment a counter variable.
The "Why": These basic algorithms form the building blocks for more complex data processing. Even though Java's Arrays class provides some search functionality, understanding how to implement these manually reinforces your looping and conditional logic skills, which are vital for adapting these patterns to more custom search or count criteria.
2. Code Examples: Searching and Counting in Arrays
Example 18.1: Searching for a Value in an Array
// Chapter18/ArraySearch.java
public class ArraySearch {
public static void main(String[] args) {
System.out.println("--- Array Search Examples ---");
int[] numbers = {10, 25, 5, 40, 15, 25, 30};
int target1 = 40;
int target2 = 99;
int target3 = 25;
// Search for target1 (40)
boolean found40 = containsValue(numbers, target1);
System.out.println("Array: " + java.util.Arrays.toString(numbers));
System.out.println("Does array contain " + target1 + "? " + found40); // Output: true
// Search for target2 (99)
boolean found99 = containsValue(numbers, target2);
System.out.println("Does array contain " + target2 + "? " + found99); // Output: false
// Search for target3 (25) and get its first index
int firstIndex25 = findFirstIndex(numbers, target3);
System.out.println("First occurrence of " + target3 + " is at index: " + firstIndex25); // Output: 1
System.out.println("-----------------------------");
}
/**
* Searches for a given target value in an integer array.
* @param arr The array to search through.
* @param target The value to search for.
* @return true if the target is found in the array, false otherwise.
*/
public static boolean containsValue(int[] arr, int target) {
// Iterate through each element of the array
for (int i = 0; i < arr.length; i++) {
// If the current element matches the target
if (arr[i] == target) {
return true; // We found it, so we can immediately return true and exit the method.
}
}
// If the loop finishes without finding the target, it means the target is not in the array.
return false;
}
/**
* Finds the index of the first occurrence of a target value in an integer array.
* @param arr The array to search through.
* @param target The value to search for.
* @return The index of the first occurrence, or -1 if the target is not found.
*/
public static int findFirstIndex(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // Return the index immediately upon finding the first match
}
}
return -1; // Conventionally return -1 if the element is not found
}
}
Example 18.2: Counting Occurrences of a Value
// Chapter18/ArrayCounting.java
public class ArrayCounting {
public static void main(String[] args) {
System.out.println("--- Array Counting Examples ---");
String[] fruits = {"apple", "banana", "apple", "orange", "grape", "apple"};
String fruit1 = "apple";
String fruit2 = "kiwi";
String fruit3 = "orange";
System.out.println("Fruits array: " + java.util.Arrays.toString(fruits));
// Count occurrences of "apple"
int appleCount = countOccurrences(fruits, fruit1);
System.out.println("Number of '" + fruit1 + "'s: " + appleCount); // Output: 3
// Count occurrences of "kiwi"
int kiwiCount = countOccurrences(fruits, fruit2);
System.out.println("Number of '" + fruit2 + "'s: " + kiwiCount); // Output: 0
// Count occurrences of "orange"
int orangeCount = countOccurrences(fruits, fruit3);
System.out.println("Number of '" + fruit3 + "'s: " + orangeCount); // Output: 1
System.out.println("-----------------------------");
}
/**
* Counts how many times a target string appears in a string array.
* @param arr The string array to search through.
* @param target The string to count.
* @return The number of times the target string appears.
*/
public static int countOccurrences(String[] arr, String target) {
int count = 0; // Initialize a counter variable
// Use for-each loop for simple iteration when index is not needed
for (String element : arr) {
// For Strings, always use .equals() for content comparison!
if (element.equals(target)) {
count++; // Increment the counter if a match is found
}
}
return count; // Return the total count after checking all elements
}
}
3. Line-by-Line Breakdown
Let's break down ArraySearch.java:
public static boolean containsValue(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) { // Loop through each index
if (arr[i] == target) { // Check if the element at current index matches target
return true; // If found, immediately return true and exit the method
}
}
return false; // If the loop completes, it means target was not found, so return false
}
And from ArrayCounting.java:
public static int countOccurrences(String[] arr, String target) {
int count = 0; // Initialize accumulator for counting
for (String element : arr) { // Iterate through each string element in the array
if (element.equals(target)) { // For String comparison, use .equals()
count++; // Increment the counter
}
}
return count; // Return the final count
}
4. Pro-Tips: Algorithmic Habits
- Linear Search for Small Data: Linear search is simple and sufficient for small arrays. For very large arrays, it becomes inefficient, and more advanced algorithms (like binary search, which requires a sorted array) are needed (future topic!).
- Return Early: In search algorithms, if you find what you're looking for,
returnimmediately. There's no need to continue iterating through the rest of the array. - Initialize Accumulators: Always initialize counter or sum variables (accumulators) to
0before starting your loops. equals()for Objects: Reiterate: Always use.equals()for comparing the content of objects (likeString), never==.
5. Unsolved Exercise: Inventory Check
You have an inventory of various items.
- Create a class named
InventoryCheck. - Declare a
Stringarray namedinventoryinitialized with at least 8 items, including some duplicates (e.g.,{"Book", "Pen", "Book", "Laptop", "Mouse", "Book", "Keyboard", "Pen"}). - Implement a
public static booleanmethoditemExists(String[] inventory, String itemName)that returnstrueifitemNameis found in theinventoryarray,falseotherwise. - Implement a
public static intmethodcountItem(String[] inventory, String itemName)that returns the total number of timesitemNameappears in theinventoryarray. - In the
mainmethod:- Test
itemExistsfor "Laptop" and "Tablet", printing the results. - Test
countItemfor "Book" and "Pen", printing the results.
- Test
6. Complete Solution: Inventory Check
// Chapter18/InventoryCheck.java
import java.util.Arrays; // For printing the array easily
public class InventoryCheck {
public static void main(String[] args) {
System.out.println("--- Inventory Check ---");
// 2. Declare and initialize the inventory array
String[] inventory = {"Book", "Pen", "Book", "Laptop", "Mouse", "Book", "Keyboard", "Pen"};
System.out.println("Current Inventory: " + Arrays.toString(inventory));
System.out.println("\n--- Item Existence Check ---");
// Test itemExists for "Laptop"
String searchItem1 = "Laptop";
boolean laptopFound = itemExists(inventory, searchItem1);
System.out.println("Does '" + searchItem1 + "' exist? " + laptopFound); // Output: true
// Test itemExists for "Tablet"
String searchItem2 = "Tablet";
boolean tabletFound = itemExists(inventory, searchItem2);
System.out.println("Does '" + searchItem2 + "' exist? " + tabletFound); // Output: false
System.out.println("\n--- Item Count Check ---");
// Test countItem for "Book"
String countItem1 = "Book";
int bookCount = countItem(inventory, countItem1);
System.out.println("Number of '" + countItem1 + "'s: " + bookCount); // Output: 3
// Test countItem for "Pen"
String countItem2 = "Pen";
int penCount = countItem(inventory, countItem2);
System.out.println("Number of '" + countItem2 + "'s: " + penCount); // Output: 2
System.out.println("-----------------------");
}
/**
* Checks if a specified item exists in the inventory array.
* @param inventory The array of inventory items.
* @param itemName The name of the item to search for.
* @return true if the item is found, false otherwise.
*/
public static boolean itemExists(String[] inventory, String itemName) {
for (String item : inventory) {
// Use .equals() for String comparison
if (item.equals(itemName)) {
return true; // Item found, no need to check further
}
}
return false; // Loop finished, item not found
}
/**
* Counts the number of occurrences of a specified item in the inventory array.
* @param inventory The array of inventory items.
* @param itemName The name of the item to count.
* @return The total number of times the item appears.
*/
public static int countItem(String[] inventory, String itemName) {
int count = 0; // Initialize the counter
for (String item : inventory) {
if (item.equals(itemName)) {
count++; // Increment count if item matches
}
}
return count; // Return the final count
}
}
Chapter 19: Basic Algorithm: Accumulators & Flags.
1. Quick Theory: Tracking State in Loops
These are two simple yet powerful patterns you'll use constantly when writing algorithms:
- Accumulators: An accumulator is a variable that is used to "accumulate" a value over multiple iterations of a loop. This could be a sum (e.g., total score), a product (e.g., factorial), a count (e.g., number of positive values), or even building up a string. You typically initialize the accumulator before the loop and update it inside the loop.
- Boolean Flags: A boolean flag is a
booleanvariable (initialized totrueorfalse) that acts like a switch. Its value changes during the execution of a loop or method to indicate that a specific condition has been met or a particular event has occurred. Flags are excellent for:- Signaling success or failure of an operation.
- Determining if a specific condition was ever met within a loop.
- Controlling the flow of a program after a loop finishes.
- Sometimes, even stopping a loop early.
The "Why": Accumulators simplify calculations over collections, allowing you to derive single summary values. Boolean flags provide a clean and readable way to track state changes and make decisions based on events that happen within a process, especially within loops, without requiring complex nested conditions. They make your algorithms easier to reason about and maintain.
2. Code Examples: Accumulators and Flags in Harmony
Example 19.1: Summing and Detecting Negatives
// Chapter19/AccumulatorAndFlagExample.java
public class AccumulatorAndFlagExample {
public static void main(String[] args) {
System.out.println("--- Accumulator and Flag Example ---");
int[] numbers = {5, 10, -3, 20, -8, 15, 0};
System.out.println("Numbers array: " + java.util.Arrays.toString(numbers));
// 1. Accumulator: Calculating the sum of positive numbers
int sumOfPositives = 0; // Initialize accumulator
// 2. Boolean Flag: To check if any negative number was encountered
boolean foundNegative = false; // Initialize flag to false (assume no negatives yet)
System.out.println("\nProcessing numbers...");
for (int number : numbers) {
if (number > 0) {
sumOfPositives += number; // Accumulate positive numbers
} else if (number < 0) {
foundNegative = true; // Set the flag to true if a negative number is found
System.out.println(" Negative number encountered: " + number);
}
}
System.out.println("\nSummary:");
System.out.println("Sum of positive numbers: " + sumOfPositives); // Output: 5 + 10 + 20 + 15 = 50
// Use the flag to make a decision after the loop
if (foundNegative) {
System.out.println("At least one negative number was present in the array.");
} else {
System.out.println("No negative numbers were found in the array.");
}
System.out.println("------------------------------------");
}
}
Example 19.2: Flag to Control Loop or Indicate First Match
// Chapter19/LoopControlWithFlag.java
import java.util.Scanner;
public class LoopControlWithFlag {
public static void main(String[] args) {
System.out.println("--- Loop Control with Flag ---");
String[] usernames = {"alice", "bob", "charlie", "diana", "frank"};
Scanner scanner = new Scanner(System.in);
// Flag to indicate if a username was found
boolean userFound = false;
System.out.print("Enter a username to search for: ");
String searchName = scanner.nextLine();
// Loop through usernames to find a match
for (String user : usernames) {
if (user.equals(searchName.toLowerCase())) { // Convert input to lowercase for case-insensitive comparison
userFound = true; // Set flag to true if a match is found
System.out.println("User '" + searchName + "' found in the system!");
break; // 'break' keyword immediately exits the loop (optimization)
}
}
// Use the flag after the loop to provide a final message
if (!userFound) { // If the flag is still false
System.out.println("User '" + searchName + "' was not found in the system.");
}
System.out.println("\n--- Loop Stopped by Flag (Conceptual) ---");
// Example of a flag to stop a "processing" loop early based on a condition
int processCounter = 0;
boolean criticalErrorDetected = false;
while (processCounter < 10 && !criticalErrorDetected) { // Loop while counter is low AND no error
System.out.println("Processing step " + (processCounter + 1));
// Simulate a condition that might set the flag
if (processCounter == 3) {
System.out.println(" Simulating a critical error at step " + (processCounter + 1) + "!");
criticalErrorDetected = true; // Set the flag to true
}
processCounter++;
}
if (criticalErrorDetected) {
System.out.println("Process halted due to critical error after " + processCounter + " steps.");
} else {
System.out.println("Process completed successfully through all steps.");
}
System.out.println("------------------------------------");
scanner.close();
}
}
3. Line-by-Line Breakdown
Let's break down AccumulatorAndFlagExample.java:
int sumOfPositives = 0; // Accumulator: Initialized to 0 before the loop.
boolean foundNegative = false; // Flag: Initialized to false, assuming no negatives yet.
for (int number : numbers) {
if (number > 0) {
sumOfPositives += number; // Accumulator updated: add positive numbers to the sum.
} else if (number < 0) {
foundNegative = true; // Flag changed: once a negative is found, set it to true.
// It stays true even if more negatives are found or the loop continues.
}
}
if (foundNegative) { // Decision based on the flag's final state after the loop.
System.out.println("At least one negative number was present in the array.");
}
And from LoopControlWithFlag.java:
boolean userFound = false; // Flag for search result
for (String user : usernames) {
if (user.equals(searchName.toLowerCase())) {
userFound = true; // Set flag to true
break; // 'break' keyword: Exits the loop immediately.
// This is an optimization; if we found what we need, no reason to continue looping.
}
}
if (!userFound) { // Check the flag's state after the loop
System.out.println("User '" + searchName + "' was not found in the system.");
}
// Second example:
boolean criticalErrorDetected = false; // Flag for error state
while (processCounter < 10 && !criticalErrorDetected) { // Loop condition incorporates the flag
// The loop continues as long as processCounter is less than 10 AND criticalErrorDetected is false.
// If criticalErrorDetected becomes true inside the loop, the condition !criticalErrorDetected becomes false,
// and the loop terminates in the next iteration check.
if (processCounter == 3) {
criticalErrorDetected = true; // Set the flag
}
processCounter++;
}
4. Pro-Tips: Mastering Accumulators and Flags
- Initialize Correctly: Always initialize accumulators (
sum = 0,product = 1,count = 0) and flags (found = false,error = false) to their correct starting values before the loop. - Clear Purpose: Each flag should have a clear, single purpose. Avoid trying to make one flag represent too many different states.
- Readability: Flags often make your
ifconditions and loop control clearer. Instead of a complex boolean expression, you can just checkif (isDataValid)orwhile (!isFinished). breakfor Optimization: When using a flag to indicate that something has been found (like in a search), consider usingbreakto exit the loop early once the flag is set. This improves efficiency.
5. Unsolved Exercise: Student Attendance Tracker
You need to track student attendance and summarize the results.
- Create a class named
AttendanceTracker. - Declare a
booleanarray namedattendancefor 5 days, representing whether a student was present (true) or absent (false). Initialize it with a mix oftrueandfalsevalues (e.g.,{true, false, true, true, false}). - Use an accumulator to count the
totalPresentDays. - Use a boolean flag
hadPerfectAttendanceto check if the student was present every single day. Initialize it totrue. If anyfalse(absence) is encountered, set this flag tofalse. - After the loop, print the
totalPresentDays. - Then, use the
hadPerfectAttendanceflag to print whether the student had perfect attendance or not.
6. Complete Solution: Student Attendance Tracker
// Chapter19/AttendanceTracker.java
import java.util.Arrays; // For printing the array easily
public class AttendanceTracker {
public static void main(String[] args) {
System.out.println("--- Student Attendance Tracker ---");
// 2. Declare and initialize the attendance array for 5 days
boolean[] attendance = {true, false, true, true, true}; // Student was absent on Day 2
System.out.println("Daily Attendance: " + Arrays.toString(attendance));
// 3. Accumulator: Count total present days
int totalPresentDays = 0; // Initialize sum to 0
// 4. Boolean Flag: Check for perfect attendance
boolean hadPerfectAttendance = true; // Assume perfect attendance initially
System.out.println("\nProcessing attendance records...");
for (int i = 0; i < attendance.length; i++) {
if (attendance[i]) { // If student was present (value is true)
totalPresentDays++; // Increment the accumulator
System.out.println("Day " + (i + 1) + ": Present");
} else { // If student was absent (value is false)
hadPerfectAttendance = false; // Set flag to false (can't be perfect if even one absence)
System.out.println("Day " + (i + 1) + ": Absent");
}
}
System.out.println("\n--- Attendance Summary ---");
// 5. Print total present days
System.out.println("Total days present: " + totalPresentDays + " out of " + attendance.length);
// 6. Use the flag to print perfect attendance status
if (hadPerfectAttendance) {
System.out.println("Congratulations! The student had perfect attendance.");
} else {
System.out.println("The student did NOT have perfect attendance.");
}
System.out.println("--------------------------");
}
}
Book 1: Part 4 (Data Manipulation & String Mastery)
Chapter 20: Advanced String Methods.
1. Technical Theory: Precision Text Manipulation
In the real world, data rarely comes in perfectly formatted numbers. Often, it's text, and you need to extract specific pieces, clean it up, or transform it. This is where advanced String methods become indispensable. We briefly touched upon length(), toUpperCase(), and charAt(). Now, let's look at more powerful tools:
split(String regex): This method is a workhorse. It breaks a string into an array of substrings based on a specified delimiter (a "regular expression," but for now, think of it as the character or sequence of characters that separates your data).- Efficiency Focus: Manually parsing a string by finding delimiters with
indexOf()and extracting withsubstring()in a loop is tedious and error-prone.split()does all that complex work for you in one efficient call, abstracting away the looping and indexing logic.
- Efficiency Focus: Manually parsing a string by finding delimiters with
replace(CharSequence target, CharSequence replacement): This method replaces all occurrences of a specified sequence of characters (thetarget) with another sequence (thereplacement).- Efficiency Focus: While you could write a loop to find and replace characters,
replace()is highly optimized and much simpler to use for this common task.
- Efficiency Focus: While you could write a loop to find and replace characters,
trim(): Removes leading and trailing whitespace (spaces, tabs, newlines) from a string. Essential for cleaning user input or data read from files.contains(CharSequence s): Checks if a string contains a specific sequence of characters. Returnstrueorfalse.
The "Why": These methods are crucial for data parsing and data cleaning. When you read data from files (like CSVs), databases, or user input, it's often a single string with multiple pieces of information separated by special characters. Knowing split() allows you to effortlessly break these down into individual variables, making your program much more flexible and able to process real-world data formats. trim(), replace(), and contains() further empower you to prepare and validate this text data.
2. Professional Code: String Manipulation Showcase
Example 20.1: Parsing Data with split()
// Chapter20/DataParser.java
public class DataParser {
public static void main(String[] args) {
System.out.println("--- Data Parsing with split() ---");
// Simulate a line of data from a CSV file (Comma Separated Values)
String csvLine = "Laptop;1200.50;5;Electronics;True";
// Let's assume the format is: Name;Price;Quantity;Category;Available
System.out.println("Original CSV Line: " + csvLine);
// 1. Using split() to break the string into an array of substrings
// The delimiter is ";"
String[] dataParts = csvLine.split(";");
System.out.println("Number of data parts: " + dataParts.length); // Output: 5
// 2. Accessing and converting individual parts
// Always check array bounds before accessing elements!
if (dataParts.length == 5) {
String productName = dataParts[0];
double productPrice = Double.parseDouble(dataParts[1]); // Convert String to double
int productQuantity = Integer.parseInt(dataParts[2]); // Convert String to int
String productCategory = dataParts[3];
boolean isAvailable = Boolean.parseBoolean(dataParts[4]); // Convert String to boolean
System.out.println("Product Name: " + productName);
System.out.printf("Price: $%.2f\n", productPrice);
System.out.println("Quantity: " + productQuantity);
System.out.println("Category: " + productCategory);
System.out.println("Available: " + isAvailable);
} else {
System.out.println("Error: Invalid CSV line format. Expected 5 parts.");
}
// Another example with a different delimiter (space)
String sentence = "Java is a powerful language";
String[] words = sentence.split(" "); // Split by space
System.out.println("\nWords in sentence: ");
for (String word : words) {
System.out.println("- " + word);
}
System.out.println("---------------------------------");
}
}
Example 20.2: Cleaning and Modifying Strings
// Chapter20/StringCleaner.java
public class StringCleaner {
public static void main(String[] args) {
System.out.println("--- String Cleaning and Modification ---");
// Simulate messy user input
String userInput = " Please enter your NAME ";
System.out.println("Original input: '" + userInput + "'");
// 1. Using trim(): Remove leading/trailing whitespace
String trimmedInput = userInput.trim();
System.out.println("Trimmed input: '" + trimmedInput + "'"); // Output: 'Please enter your NAME'
// 2. Using replace(): Change specific characters or substrings
String message = "Hello, World! This is a test message.";
System.out.println("\nOriginal message: '" + message + "'");
// Replace all spaces with underscores
String noSpaces = message.replace(" ", "_");
System.out.println("Spaces replaced: '" + noSpaces + "'"); // Output: Hello,_World!_This_is_a_test_message.
// Replace "test" with "sample"
String replacedWord = message.replace("test", "sample");
System.out.println("Word replaced: '" + replacedWord + "'"); // Output: Hello, World! This is a sample message.
// Replace a character
String replacedChar = message.replace('e', 'E');
System.out.println("Char replaced: '" + replacedChar + "'"); // Output: HEllO, World! This is a tEst mEssagE.
// 3. Using contains(): Check for existence of a substring
String email = "john.doe@example.com";
String searchDomain = "@example.com";
System.out.println("\nEmail: '" + email + "'");
boolean hasExampleDomain = email.contains(searchDomain);
System.out.println("Does email contain '" + searchDomain + "'? " + hasExampleDomain); // Output: true
String badWord = "badword";
String userComment = "This is a comment without badword.";
boolean commentContainsBadWord = userComment.toLowerCase().contains(badWord); // Check case-insensitively
System.out.println("Comment contains '" + badWord + "'? " + commentContainsBadWord); // Output: true
System.out.println("------------------------------------");
}
}
3. Clean Code Tip: Avoid 'Spaghetti Code' with Chaining
Instead of multiple temporary variables for string manipulations, you can often chain method calls because many String methods return a new String. This makes your code more concise and readable.
Bad (Spaghetti):
String messy = " some,data ";
String temp1 = messy.trim();
String temp2 = temp1.replace(",", "");
String cleaned = temp2.toUpperCase();
Good (Chained):
String messy = " some,data ";
String cleaned = messy.trim().replace(",", "").toUpperCase(); // Chained calls
This reduces intermediate variables and shows a clear flow of operations.
4. Unsolved Exercise: Log Entry Processor
You have a log entry string that records an event.
- Create a class named
LogProcessor. - Declare a
StringvariablelogEntrywith the value"2024-03-10 14:35:10 | ERROR | User 'admin' failed login from 192.168.1.100". - Split the
logEntryby the delimiter" | "to get individual parts. - Extract the
timestamp,level(e.g., "ERROR"), andmessageinto separateStringvariables. - In the
messagepart, extract theusername(e.g., "admin") by splitting the message further. You might need to usereplace()orsubstring()to isolate the username. - Print the
timestamp,level, andusernameclearly labeled.
5. Complete Solution: Log Entry Processor
// Chapter20/LogProcessor.java
public class LogProcessor {
public static void main(String[] args) {
System.out.println("--- Log Entry Processor ---");
// 2. Declare logEntry string
String logEntry = "2024-03-10 14:35:10 | ERROR | User 'admin' failed login from 192.168.1.100";
System.out.println("Original Log Entry: " + logEntry);
// 3. Split the logEntry by " | "
String[] parts = logEntry.split(" \\| "); // Note: " | " needs to be escaped as " \\| " if using regex for literal "|"
// Always check if you got the expected number of parts
if (parts.length >= 3) {
// 4. Extract timestamp, level, and message
String timestamp = parts[0];
String level = parts[1];
String message = parts[2];
System.out.println("\nExtracted Parts:");
System.out.println("Timestamp: " + timestamp);
System.out.println("Level: " + level);
System.out.println("Message: " + message);
// 5. Extract username from the message part
// The message is "User 'admin' failed login from 192.168.1.100"
// We want "admin". We can find the start of ' and end of '.
int userStart = message.indexOf("'") + 1; // Find first ' and move past it
int userEnd = message.indexOf("'", userStart); // Find next ' starting from userStart
String username = "N/A"; // Default value
if (userStart > 0 && userEnd > userStart) {
username = message.substring(userStart, userEnd);
}
// 6. Print the extracted information
System.out.println("Username: " + username);
} else {
System.out.println("Error: Log entry format unexpected. Could not split into 3 parts.");
}
System.out.println("---------------------------");
}
}
Chapter 21: String Comparison Deep Dive.
1. Technical Theory: Precise String Order and Equality
We've already learned that == compares object references (memory addresses) and .equals() compares the actual content of strings. However, sometimes you need more: case-insensitive comparison or to know how strings order alphabetically.
equalsIgnoreCase(String anotherString): This method is similar toequals(), but it ignores case differences. "Hello" and "hello" would be considered equal.- Efficiency Focus: While you could manually convert both strings to uppercase or lowercase before using
equals(),equalsIgnoreCase()is a dedicated, optimized method for this common task. It's cleaner and more efficient thanmyString.toLowerCase().equals(anotherString.toLowerCase()).
- Efficiency Focus: While you could manually convert both strings to uppercase or lowercase before using
compareTo(String anotherString): This method is crucial for determining the lexicographical order (alphabetical order) of strings. It returns anintvalue:- A negative integer if the current string comes before
anotherStringalphabetically. - Zero if the current string is alphabetically equal to
anotherString. - A positive integer if the current string comes after
anotherStringalphabetically. - Efficiency Focus: Manually comparing strings character by character is complex.
compareTo()provides a standardized, efficient way to do this. This is the method Java (and you) will use internally when sorting arrays of strings.
- A negative integer if the current string comes before
The "Why": Precise string comparison is vital for sorting lists (alphabetically), searching (finding matches regardless of case), validation (e.g., matching a username), and many other data processing tasks. compareTo() is fundamental to understanding how collections of strings are ordered, which is a key requirement for any data management system.
2. Professional Code: String Comparison in Action
Example 21.1: Equality and Case Sensitivity
// Chapter21/StringEquality.java
public class StringEquality {
public static void main(String[] args) {
System.out.println("--- String Equality Comparisons ---");
String s1 = "Java";
String s2 = "java";
String s3 = "Java";
String s4 = new String("Java"); // Creates a new String object
System.out.println("s1: '" + s1 + "'");
System.out.println("s2: '" + s2 + "'");
System.out.println("s3: '" + s3 + "'");
System.out.println("s4: '" + s4 + "'");
System.out.println("---------------------------------");
// 1. Using equals() - Case-sensitive content comparison
System.out.println("s1.equals(s2) (Java vs java): " + s1.equals(s2)); // Output: false
System.out.println("s1.equals(s3) (Java vs Java): " + s1.equals(s3)); // Output: true
System.out.println("s1.equals(s4) (Java vs new Java): " + s1.equals(s4)); // Output: true (content is same)
// 2. Using equalsIgnoreCase() - Case-insensitive content comparison
System.out.println("\ns1.equalsIgnoreCase(s2) (Java vs java): " + s1.equalsIgnoreCase(s2)); // Output: true
System.out.println("s1.equalsIgnoreCase(s3) (Java vs Java): " + s1.equalsIgnoreCase(s3)); // Output: true
// Reminder: == compares references, not content
System.out.println("\ns1 == s2: " + (s1 == s2)); // Output: false (different objects, different content)
System.out.println("s1 == s3: " + (s1 == s3)); // Output: true (String Pool optimization)
System.out.println("s1 == s4: " + (s1 == s4)); // Output: false (s4 is a new object)
System.out.println("---------------------------------");
}
}
Example 21.2: Ordering Strings with compareTo() and Sorting an Array
// Chapter21/StringOrdering.java
import java.util.Arrays; // Needed for Arrays.sort() and Arrays.toString()
public class StringOrdering {
public static void main(String[] args) {
System.out.println("--- String Ordering with compareTo() ---");
String fruit1 = "Apple";
String fruit2 = "Banana";
String fruit3 = "apple";
String fruit4 = "Orange";
System.out.println("Comparing '" + fruit1 + "' and '" + fruit2 + "': " + fruit1.compareTo(fruit2));
// Output: Negative number (Apple comes before Banana)
System.out.println("Comparing '" + fruit2 + "' and '" + fruit1 + "': " + fruit2.compareTo(fruit1));
// Output: Positive number (Banana comes after Apple)
System.out.println("Comparing '" + fruit1 + "' and '" + fruit3 + "': " + fruit1.compareTo(fruit3));
// Output: Positive number (Uppercase 'A' comes before lowercase 'a' in ASCII/Unicode,
// so "Apple" is considered "greater" than "apple")
System.out.println("Comparing '" + fruit3 + "' and '" + fruit1 + "': " + fruit3.compareTo(fruit1));
// Output: Negative number
System.out.println("Comparing 'hello' and 'hello': " + "hello".compareTo("hello")); // Output: 0
// Use compareToIgnoreCase() for case-insensitive ordering
System.out.println("\nComparing '" + fruit1 + "' and '" + fruit3 + "' (ignore case): " + fruit1.compareToIgnoreCase(fruit3));
// Output: 0 (Apple is equal to apple when ignoring case)
System.out.println("\n--- Sorting an Array of Strings ---");
String[] names = {"Charlie", "Alice", "Bob", "frank", "David"};
System.out.println("Original names: " + Arrays.toString(names));
// Arrays.sort() uses compareTo() internally for String arrays
Arrays.sort(names);
System.out.println("Sorted names: " + Arrays.toString(names));
// Output: [Alice, Bob, Charlie, David, frank]
// Note: 'frank' comes after 'David' because lowercase 'f' comes after uppercase 'D' in ASCII/Unicode.
// For purely alphabetical regardless of case, you'd need a custom Comparator (advanced topic).
System.out.println("------------------------------------");
}
}
3. Clean Code Tip: Always Be Explicit About Case-Sensitivity
When comparing strings, assume you need to be case-sensitive unless there's a specific reason not to be. If you do want case-insensitivity, use equalsIgnoreCase() or compareToIgnoreCase(). Never rely on converting both strings to .toLowerCase() or .toUpperCase() manually just for comparison if a dedicated method exists; it's less efficient and less readable.
4. Unsolved Exercise: User Login & Name Sorter
You're building a simple user system.
- Create a class named
UserSystem. - Declare a
StringvariablestoredUsernamewith the value"admin". - Ask the user to enter their
usernameusingScanner. - Compare the
enteredUsernamewithstoredUsernameusingequalsIgnoreCase(). If they match, print "Login successful!". Otherwise, print "Login failed.". - Declare a
StringarrayuserListwith 4-5 names, some with different casing (e.g.,"Zoe", "adam", "eve", "chris", "BOB"). - Print the
userListbefore sorting. - Sort the
userListalphabetically usingArrays.sort(). - Print the
userListafter sorting. Comment on why the order might not be strictly alphabetical if casing differs.
6. Complete Solution: User Login & Name Sorter
// Chapter21/UserSystem.java
import java.util.Arrays;
import java.util.Scanner;
public class UserSystem {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("--- User Login & Name Sorter ---");
// 2. Stored username
String storedUsername = "admin";
// 3. Ask user for username
System.out.print("Enter your username: ");
String enteredUsername = scanner.nextLine();
// 4. Compare using equalsIgnoreCase()
if (enteredUsername.equalsIgnoreCase(storedUsername)) {
System.out.println("Login successful!");
} else {
System.out.println("Login failed. Incorrect username.");
}
System.out.println("\n--- User List Sorting ---");
// 5. Declare and initialize userList array
String[] userList = {"Zoe", "adam", "eve", "chris", "BOB"};
System.out.println("Original user list: " + Arrays.toString(userList));
// 7. Sort the userList
Arrays.sort(userList);
// 8. Print sorted userList
System.out.println("Sorted user list: " + Arrays.toString(userList));
// Comment: The sorted order might not be strictly alphabetical from a human perspective
// if names have different casing (e.g., 'BOB' might come before 'chris' or after 'adam').
// This is because Arrays.sort() for Strings uses compareTo(), which is case-sensitive
// and orders based on Unicode values (uppercase letters come before lowercase letters).
// For true case-insensitive alphabetical sorting, a custom Comparator is needed (advanced).
System.out.println("------------------------------");
scanner.close();
}
}
Chapter 22: Searching Algorithms.
1. Technical Theory: The Quest for Data - Linear Search
The most straightforward way to find an element within an array is called a linear search. This algorithm does exactly what its name suggests: it checks each element in the array, one by one, from beginning to end, until it finds a match or reaches the end of the array.
- Logic: Start at the first element (index 0). Compare it to your
targetvalue. If it matches, you're done! Return its index. If not, move to the next element and repeat. If you've checked every element and haven't found thetarget, it means the element isn't in the array. In this case, it's common practice to return-1to indicate "not found," as-1is never a valid array index. - Efficiency: Linear search is simple to implement but can be inefficient for very large arrays. In the worst-case scenario (the element is at the very end, or not in the array at all), it has to check every single element. Its time complexity is O(n), meaning the time it takes grows linearly with the size (n) of the array.
The "Why": While more efficient search algorithms exist (like binary search, which requires a sorted array), understanding linear search is fundamental. It teaches you how to systematically iterate through a collection to find data, a pattern that forms the basis for many other algorithms. For small arrays, its simplicity often outweighs the need for more complex, faster algorithms.
2. Professional Code: Implementing Linear Search
Example 22.1: Linear Search for Integers
// Chapter22/LinearSearchInt.java
public class LinearSearchInt {
public static void main(String[] args) {
System.out.println("--- Linear Search for Integers ---");
int[] scores = {85, 92, 78, 95, 60, 88, 72};
int targetScore1 = 95;
int targetScore2 = 78;
int targetScore3 = 100; // Not in the array
System.out.println("Scores: " + java.util.Arrays.toString(scores));
// Test 1: Search for 95
int index1 = linearSearch(scores, targetScore1);
if (index1 != -1) {
System.out.println(targetScore1 + " found at index: " + index1); // Output: 3
} else {
System.out.println(targetScore1 + " not found.");
}
// Test 2: Search for 78
int index2 = linearSearch(scores, targetScore2);
if (index2 != -1) {
System.out.println(targetScore2 + " found at index: " + index2); // Output: 2
} else {
System.out.println(targetScore2 + " not found.");
}
// Test 3: Search for 100
int index3 = linearSearch(scores, targetScore3);
if (index3 != -1) {
System.out.println(targetScore3 + " found at index: " + index3);
} else {
System.out.println(targetScore3 + " not found."); // Output: 100 not found.
}
System.out.println("----------------------------------");
}
/**
* Performs a linear search on an integer array to find the target value.
* @param arr The array to search within.
* @param target The integer value to search for.
* @return The index of the first occurrence of the target, or -1 if not found.
*/
public static int linearSearch(int[] arr, int target) {
// Iterate through the array from the first element to the last
for (int i = 0; i < arr.length; i++) {
// Check if the current element matches the target
if (arr[i] == target) {
return i; // If a match is found, return its index immediately
}
}
// If the loop completes without returning, it means the target was not found
return -1; // Indicate that the element was not found
}
}
Example 22.2: Linear Search for Strings (Case-Insensitive)
// Chapter22/LinearSearchString.java
public class LinearSearchString {
public static void main(String[] args) {
System.out.println("--- Linear Search for Strings (Case-Insensitive) ---");
String[] products = {"Laptop", "Mouse", "Keyboard", "Monitor", "Webcam"};
String searchProduct1 = "monitor"; // Different casing
String searchProduct2 = "Speaker"; // Not in the array
String searchProduct3 = "Mouse";
System.out.println("Products: " + java.util.Arrays.toString(products));
// Test 1: Search for "monitor" (case-insensitive)
int index1 = linearSearchIgnoreCase(products, searchProduct1);
if (index1 != -1) {
System.out.println("'" + searchProduct1 + "' found at index: " + index1); // Output: 3
} else {
System.out.println("'" + searchProduct1 + "' not found.");
}
// Test 2: Search for "Speaker"
int index2 = linearSearchIgnoreCase(products, searchProduct2);
if (index2 != -1) {
System.out.println("'" + searchProduct2 + "' found at index: " + index2);
} else {
System.out.println("'" + searchProduct2 + "' not found."); // Output: 'Speaker' not found.
}
// Test 3: Search for "Mouse" (exact casing match)
int index3 = linearSearchIgnoreCase(products, searchProduct3);
if (index3 != -1) {
System.out.println("'" + searchProduct3 + "' found at index: " + index3); // Output: 1
} else {
System.out.println("'" + searchProduct3 + "' not found.");
}
System.out.println("----------------------------------------------");
}
/**
* Performs a linear search on a String array, ignoring case, to find the target string.
* @param arr The array of strings to search within.
* @param target The string value to search for (case-insensitive).
* @return The index of the first occurrence of the target, or -1 if not found.
*/
public static int linearSearchIgnoreCase(String[] arr, String target) {
// Iterate through the array using a standard for loop to get indices
for (int i = 0; i < arr.length; i++) {
// Use equalsIgnoreCase() for case-insensitive comparison
if (arr[i].equalsIgnoreCase(target)) {
return i; // Return the index if a match is found
}
}
return -1; // Target not found after checking all elements
}
}
3. Clean Code Tip: Abstract Search Logic into Methods
Always encapsulate your search logic within a dedicated method (like linearSearch or linearSearchIgnoreCase). This promotes code reusability, makes your main method cleaner, and allows you to easily test your search logic independently. Passing the array and target as parameters makes the method flexible.
4. Unsolved Exercise: Student ID Finder
You have a list of student IDs.
- Create a class named
StudentIdFinder. - Declare an
intarraystudentIDswith at least 6 unique student ID numbers (e.g.,101, 105, 110, 112, 115, 120). - Implement a
public static intmethodfindStudent(int[] ids, int targetId)that performs a linear search fortargetIdin theidsarray. It should return the index if found, and-1otherwise. - In the
mainmethod, testfindStudentfortargetId = 110andtargetId = 100. Print messages indicating whether the student was found and, if so, at which index.
5. Complete Solution: Student ID Finder
// Chapter22/StudentIdFinder.java
public class StudentIdFinder {
public static void main(String[] args) {
System.out.println("--- Student ID Finder ---");
// 2. Declare and initialize studentIDs array
int[] studentIDs = {101, 105, 110, 112, 115, 120};
System.out.println("Student IDs: " + java.util.Arrays.toString(studentIDs));
// Test 1: Search for an ID that exists
int targetId1 = 110;
int index1 = findStudent(studentIDs, targetId1);
if (index1 != -1) {
System.out.println("Student ID " + targetId1 + " found at index: " + index1); // Output: 2
} else {
System.out.println("Student ID " + targetId1 + " not found.");
}
// Test 2: Search for an ID that does not exist
int targetId2 = 100;
int index2 = findStudent(studentIDs, targetId2);
if (index2 != -1) {
System.out.println("Student ID " + targetId2 + " found at index: " + index2);
} else {
System.out.println("Student ID " + targetId2 + " not found."); // Output: -1
}
System.out.println("-------------------------");
}
/**
* Performs a linear search for a target student ID in an array of IDs.
* @param ids The array of student IDs to search within.
* @param targetId The ID number to search for.
* @return The index of the target ID if found, or -1 if not found.
*/
public static int findStudent(int[] ids, int targetId) {
// Loop through each element of the IDs array
for (int i = 0; i < ids.length; i++) {
// Check if the current ID matches the target ID
if (ids[i] == targetId) {
return i; // Return the index immediately upon finding a match
}
}
// If the loop completes, the target ID was not found
return -1;
}
}
Chapter 23: Sorting Fundamentals.
1. Technical Theory: Ordering Your Data - Bubble Sort
Sorting is the process of arranging elements in an array (or list) into a specific order, such as ascending (smallest to largest) or descending (largest to smallest). It's one of the most common and important operations in computer science. There are many sorting algorithms, each with its own efficiency characteristics.
For your DAM exams, you must understand Bubble Sort. While it's one of the simplest sorting algorithms to understand and implement, it's also highly inefficient for large datasets.
- Logic (How it works):
- Bubble Sort repeatedly steps through the list.
- It compares adjacent elements and swaps them if they are in the wrong order.
- The pass through the list is repeated until no swaps are needed, which indicates that the list is sorted.
- With each pass, the largest unsorted element "bubbles" up to its correct position at the end of the unsorted portion of the array.
- The 'temp' variable swap: To swap two values (e.g.,
aandb), you need a temporary variable:int temp = a; a = b; b = temp; - Efficiency: Bubble Sort has a time complexity of O(n^2), which means for an array of
nelements, it might perform roughlyn*ncomparisons. For an array of 1000 elements, that's a million comparisons in the worst case! This is why professional developers useArrays.sort()(which employs much faster algorithms like Quicksort or Timsort).
The "Why": You need to know Bubble Sort not because you'll use it in production code, but because it's an excellent exercise in understanding nested loops, array manipulation, and the fundamental concept of comparison and swapping elements. It's often a mandatory question to assess your grasp of basic algorithmic logic for exams like DAM.
2. Professional Code: Bubble Sort Implementation
Example 23.1: Sorting an Integer Array with Bubble Sort
// Chapter23/BubbleSortInt.java
import java.util.Arrays; // For printing the array easily
public class BubbleSortInt {
public static void main(String[] args) {
System.out.println("--- Bubble Sort for Integers ---");
int[] numbers = {64, 34, 25, 12, 22, 11, 90};
System.out.println("Original array: " + Arrays.toString(numbers));
bubbleSort(numbers); // Call the sorting method
System.out.println("Sorted array: " + Arrays.toString(numbers)); // Output: [11, 12, 22, 25, 34, 64, 90]
System.out.println("------------------------------");
int[] anotherArray = {5, 1, 4, 2, 8};
System.out.println("\nAnother original array: " + Arrays.toString(anotherArray));
bubbleSort(anotherArray);
System.out.println("Another sorted array: " + Arrays.toString(anotherArray)); // Output: [1, 2, 4, 5, 8]
System.out.println("------------------------------");
}
/**
* Sorts an integer array in ascending order using the Bubble Sort algorithm.
* This method modifies the input array directly (in-place sort).
* @param arr The integer array to be sorted.
*/
public static void bubbleSort(int[] arr) {
int n = arr.length; // Get the number of elements in the array
// Outer loop: This loop controls the number of passes.
// In each pass, the largest unsorted element bubbles to its correct position.
// We need n-1 passes because after n-1 elements are in place, the last one must also be.
for (int i = 0; i < n - 1; i++) {
// Inner loop: This loop performs the comparisons and swaps for the current pass.
// It goes from the first element up to the (n-1-i)-th element.
// The '-i' is because the last 'i' elements are already sorted and don't need to be checked again.
for (int j = 0; j < n - 1 - i; j++) {
// Compare adjacent elements
if (arr[j] > arr[j + 1]) {
// If the current element is greater than the next element, swap them.
// This is the "bubbling" action.
// SWAP using a temporary variable:
int temp = arr[j]; // 1. Store the value of arr[j] in 'temp'
arr[j] = arr[j + 1]; // 2. Overwrite arr[j] with the value of arr[j+1]
arr[j + 1] = temp; // 3. Assign the stored 'temp' value to arr[j+1]
}
}
// Optional: Print array state after each pass for visualization (uncomment to see)
// System.out.println(" After pass " + (i + 1) + ": " + Arrays.toString(arr));
}
}
}
3. Clean Code Tip: Comment the Inner Workings of Algorithms
For complex or less intuitive algorithms like Bubble Sort, it's highly beneficial to add comments within the method to explain the logic of the loops, conditions, and especially the swap operation. This helps anyone (including your future self) understand how the algorithm works without having to trace it meticulously.
4. Unsolved Exercise: Sorting Decimal Numbers
You have an array of daily stock prices that you need to sort.
- Create a class named
StockPriceSorter. - Declare a
doublearray namedstockPricesand initialize it with at least 7 arbitrary decimal values (e.g.,10.5, 8.2, 12.0, 9.1, 7.8, 11.5, 9.9). - Implement a
public static voidmethodbubbleSortDoubles(double[] arr)that sorts thedoublearray in ascending order using the Bubble Sort algorithm. - In the
mainmethod, print thestockPricesarray before and after sorting using yourbubbleSortDoublesmethod.
5. Complete Solution: Sorting Decimal Numbers
// Chapter23/StockPriceSorter.java
import java.util.Arrays; // For printing the array easily
public class StockPriceSorter {
public static void main(String[] args) {
System.out.println("--- Stock Price Sorter (Bubble Sort) ---");
// 2. Declare and initialize the double array stockPrices
double[] stockPrices = {10.5, 8.2, 12.0, 9.1, 7.8, 11.5, 9.9};
System.out.println("Original stock prices: " + Arrays.toString(stockPrices));
// Call the bubbleSortDoubles method to sort the array
bubbleSortDoubles(stockPrices);
System.out.println("Sorted stock prices: " + Arrays.toString(stockPrices));
System.out.println("----------------------------------------");
}
/**
* Sorts a double array in ascending order using the Bubble Sort algorithm.
* This method modifies the input array directly (in-place sort).
* @param arr The double array to be sorted.
*/
public static void bubbleSortDoubles(double[] arr) {
int n = arr.length; // Get the number of elements in the array
// Outer loop for passes
for (int i = 0; i < n - 1; i++) {
// Inner loop for comparisons and swaps in the current pass
for (int j = 0; j < n - 1 - i; j++) {
// Compare adjacent elements (arr[j] with arr[j+1])
if (arr[j] > arr[j + 1]) {
// Swap arr[j] and arr[j+1] if they are in the wrong order
double temp = arr[j]; // Store current element
arr[j] = arr[j + 1]; // Overwrite current with next
arr[j + 1] = temp; // Place stored element in next position
}
}
}
}
}
Chapter 24: Constants and Enums.
1. Technical Theory: Defining Fixed Values with Clarity and Safety
In programming, you often encounter values that should never change (constants) or categories that have a fixed, limited set of options. Relying on "magic strings" (e.g., "MONDAY", "ADMIN") or "magic numbers" (e.g., 1, 2, 3 to represent roles) for these is a common source of bugs and poor code readability.
- Constants (
finalkeyword): Thefinalkeyword in Java is used to declare a variable as a constant. Once initialized, its value cannot be changed. For class-level constants that are shared by all instances and directly accessible, we usepublic static final. By convention, constant names are inSCREAMING_SNAKE_CASE(all caps with underscores).- Why better than magic numbers/strings: Improves readability (e.g.,
MAX_ATTEMPTSis clearer than3), makes code easier to modify (change the constant value in one place, not everywhere), and prevents accidental modification.
- Why better than magic numbers/strings: Improves readability (e.g.,
- Enums (
enumkeyword): Anenum(short for enumeration) is a special data type that allows you to define a set of named constants. It's ideal for situations where a variable can only take one of a fixed set of predefined values. Think of days of the week, months, cardinal directions, or user roles.- Why better than Strings/ints:
- Type Safety: An enum variable can only hold one of the predefined enum constants. This prevents typos and invalid values at compile time, reducing runtime errors. If you tried to assign
"Tuesday"to aDayOfWeekenum variable, the compiler would stop you. - Readability:
DayOfWeek.MONDAYis far more descriptive than1or"Monday". - Predictability: IDEs can suggest enum values, making development faster and less error-prone.
- Clarity: Enums clearly document the valid options for a variable.
- Type Safety: An enum variable can only hold one of the predefined enum constants. This prevents typos and invalid values at compile time, reducing runtime errors. If you tried to assign
- Why better than Strings/ints:
The "Why": Using final constants and enum types is a hallmark of professional, robust Java code. They eliminate ambiguity, improve type safety, prevent common bugs arising from typos, and significantly enhance the readability and maintainability of your applications. It’s critical for DAM students to grasp this for clean data modeling.
2. Professional Code: Constants and Enums
Example 24.1: Using final Constants
// Chapter24/ConstantsExample.java
public class ConstantsExample {
// Declaring public static final constants
// These are accessible directly using the class name, e.g., ConstantsExample.MAX_RETRIES
public static final int MAX_RETRIES = 3;
public static final double PI = 3.14159; // Better to use Math.PI, but for demonstration
public static final String DEFAULT_USERNAME = "guest";
public static void main(String[] args) {
System.out.println("--- Using Constants ---");
System.out.println("Maximum login retries allowed: " + MAX_RETRIES);
System.out.println("Value of PI (approx): " + PI);
System.out.println("Default user: " + DEFAULT_USERNAME);
// Attempting to change a final constant will result in a compile-time error
// MAX_RETRIES = 5; // ERROR: cannot assign a value to final variable MAX_RETRIES
// Using constants in logic
int attemptsMade = 1;
if (attemptsMade < MAX_RETRIES) {
System.out.println("You have " + (MAX_RETRIES - attemptsMade) + " retries left.");
} else {
System.out.println("No retries left. Account locked.");
}
System.out.println("-----------------------");
}
}
Example 24.2: Defining and Using an enum
// Chapter24/TrafficLight.java
// 1. Define the enum (usually in its own file, but can be inside a class for simple examples)
// Enum constants are implicitly public static final
enum TrafficLightState {
RED, // Represents a red light state
YELLOW, // Represents a yellow light state
GREEN // Represents a green light state
}
public class TrafficLight {
public static void main(String[] args) {
System.out.println("--- Using Enums (Traffic Light) ---");
// 2. Declare an enum variable and assign a value
TrafficLightState currentState = TrafficLightState.RED;
System.out.println("Current Light: " + currentState); // Output: RED
// 3. Using enums in a switch statement (very common and clean!)
switch (currentState) {
case RED -> System.out.println("Stop! The light is Red.");
case YELLOW -> System.out.println("Prepare to stop! The light is Yellow.");
case GREEN -> System.out.println("Go! The light is Green.");
// No 'default' needed if all enum values are covered and compiler knows it (Java 14+)
// However, for robustness or future enum values, a default is good practice.
default -> System.out.println("Unknown light state.");
}
// Change the state
currentState = TrafficLightState.GREEN;
System.out.println("\nChanged light to: " + currentState);
if (currentState == TrafficLightState.GREEN) { // Comparing enum values with == is safe and correct
System.out.println("It is safe to proceed.");
}
// Enums have built-in methods
System.out.println("All Traffic Light States:");
for (TrafficLightState state : TrafficLightState.values()) { // .values() returns an array of all enum constants
System.out.println(" - " + state + " (Ordinal: " + state.ordinal() + ")"); // .ordinal() gets integer position (0-based)
}
// Output:
// - RED (Ordinal: 0)
// - YELLOW (Ordinal: 1)
// - GREEN (Ordinal: 2)
System.out.println("---------------------------------");
}
}
3. Clean Code Tip: Avoid 'Magic Strings' and 'Magic Numbers' at All Costs
Hardcoding strings like "admin" or numbers like 3 directly into your logic (if (role.equals("admin")), for (int i=0; i < 3; i++)) is a major source of bugs and unmaintainable code. Instead:
- Use
public static finalconstants for simple fixed values (public static final int MAX_ATTEMPTS = 3;). - Use
enumtypes for a fixed set of related options (enum UserRole { ADMIN, EDITOR, VIEWER; }). This makes your code type-safe, readable, and easier to refactor.
4. Unsolved Exercise: User Roles Enum
You need to manage user roles in a simple application.
- Create an
enumnamedUserRolewith the following predefined roles:ADMIN,MANAGER,EMPLOYEE,GUEST. - In a class named
RoleChecker, declare aUserRolevariablecurrentUserRoleand set it toUserRole.EMPLOYEE. - Use a
switchstatement withcurrentUserRoleto print a different message based on the role:ADMIN: "Full access granted."MANAGER: "Management features available."EMPLOYEE: "Standard user access."GUEST: "Limited guest access."
- Change
currentUserRoletoUserRole.ADMINand repeat theswitchstatement to show the change.
5. Complete Solution: User Roles Enum
// Chapter24/RoleChecker.java
// 1. Define the UserRole enum
enum UserRole {
ADMIN,
MANAGER,
EMPLOYEE,
GUEST
}
public class RoleChecker {
public static void main(String[] args) {
System.out.println("--- User Role Checker ---");
// 2. Declare currentUserRole and set it to EMPLOYEE
UserRole currentUserRole = UserRole.EMPLOYEE;
System.out.println("Current user's role: " + currentUserRole);
// 3. Use a switch statement to print messages based on the role
System.out.print("Access level: ");
switch (currentUserRole) {
case ADMIN -> System.out.println("Full access granted.");
case MANAGER -> System.out.println("Management features available.");
case EMPLOYEE -> System.out.println("Standard user access.");
case GUEST -> System.out.println("Limited guest access.");
// No default needed here as all enum constants are covered.
}
System.out.println("\n--- Changing User Role ---");
// 4. Change currentUserRole to ADMIN and repeat the switch
currentUserRole = UserRole.ADMIN;
System.out.println("New user's role: " + currentUserRole);
System.out.print("Access level: ");
switch (currentUserRole) {
case ADMIN -> System.out.println("Full access granted.");
case MANAGER -> System.out.println("Management features available.");
case EMPLOYEE -> System.out.println("Standard user access.");
case GUEST -> System.out.println("Limited guest access.");
}
System.out.println("-------------------------");
}
}
Book 1: Part 5 (Robustness & Data Persistence)
Chapter 25: Try-Catch-Finally.
1. Quick Theory: Ensuring Cleanup, Always.
In Chapter 17, we introduced try-catch to prevent crashes when exceptions occur. While catch blocks handle errors, sometimes you have critical operations (like closing files, database connections, or network sockets) that must happen whether an error occurs or not. Forgetting to clean up these "resources" can lead to memory leaks, corrupted data, or system instability.
This is where the finally block comes into play. The code within a finally block is guaranteed to execute, no matter what happens in the try block – whether an exception is thrown and caught, an exception is thrown and not caught, or the try block completes successfully. The "Why": The finally block is your safeguard for resource management. It ensures that crucial cleanup operations are performed consistently, making your applications more stable and preventing system-level issues like resource exhaustion. This is a core principle of robust programming.
2. Professional Code: The finally Block in Action
Example 25.1: Handling Input and Arithmetic with Cleanup
// Chapter25/RobustCalculatorWithCleanup.java
import java.util.InputMismatchException;
import java.util.Scanner;
public class RobustCalculatorWithCleanup {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int num1 = 0;
int num2 = 0;
double result = 0;
boolean inputSuccessful = false;
System.out.println("--- Robust Division Calculator ---");
// Input for first number
while (!inputSuccessful) {
System.out.print("Enter the first integer: ");
try {
num1 = scanner.nextInt();
inputSuccessful = true; // Input was valid
} catch (InputMismatchException e) {
System.out.println("Invalid input. Please enter a whole number.");
scanner.nextLine(); // Clear the invalid input from the buffer
}
}
// Input for second number
inputSuccessful = false; // Reset flag for next input
while (!inputSuccessful) {
System.out.print("Enter the second integer: ");
try {
num2 = scanner.nextInt();
inputSuccessful = true; // Input was valid
} catch (InputMismatchException e) {
System.out.println("Invalid input. Please enter a whole number.");
scanner.nextLine(); // Clear the invalid input from the buffer
}
}
// Perform division in a try block, with finally for cleanup
try {
if (num2 == 0) {
// Manually throw an ArithmeticException for clarity, though Java would do it automatically
throw new ArithmeticException("Division by zero is not allowed.");
}
result = (double) num1 / num2; // Perform division
System.out.printf("Result of %d / %d = %.2f\n", num1, num2, result);
} catch (ArithmeticException e) {
// Catch and handle the division by zero error
System.err.println("Calculation error: " + e.getMessage()); // System.err for error messages
} catch (Exception e) { // Catch any other unexpected exceptions
System.err.println("An unexpected error occurred: " + e.getMessage());
} finally {
// This block ALWAYS executes. It's perfect for closing resources.
System.out.println("--- Calculator operations finished. ---");
// If scanner was created inside try-catch, it might not be in scope here.
// Best practice is to declare it outside as done here.
scanner.close(); // Ensure the scanner resource is always closed.
System.out.println("Scanner closed.");
}
System.out.println("Program continues gracefully after try-catch-finally.");
System.out.println("------------------------------------");
}
}
Example 25.2: finally with an Uncaught Exception (Illustrative)
// Chapter25/FinallyWithUncaught.java
public class FinallyWithUncaught {
public static void main(String[] args) {
System.out.println("--- Finally with Uncaught Exception ---");
try {
System.out.println("Inside try block.");
// This line will cause an exception that is NOT explicitly caught by a catch block below.
// An ArrayIndexOutOfBoundsException will be thrown.
int[] numbers = new int[5];
System.out.println(numbers[10]); // Accessing an invalid index
System.out.println("This line will not be reached."); // This line is skipped
} catch (InputMismatchException e) { // This catch block won't match ArrayIndexOutOfBoundsException
System.out.println("Caught InputMismatchException: " + e.getMessage());
} finally {
// IMPORTANT: Even though ArrayIndexOutOfBoundsException is NOT caught here,
// the 'finally' block *still executes* before the program crashes.
System.out.println("Finally block executed. Performing cleanup.");
// Imagine closing a file or network connection here.
}
// The program will crash here because the ArrayIndexOutOfBoundsException was not handled.
// However, the 'finally' block still had a chance to execute.
System.out.println("This line will NOT be reached if exception is uncaught.");
System.out.println("---------------------------------------");
}
}
3. Pro-Tips: Defensive Programming with finally
- Resource Closure: The primary use of
finallyis to close resources (likeScanner, file streams, network connections, database connections) that were opened in thetryblock. This prevents resource leaks. - Declare Resources Outside: When using
finallyfor resource closure, declare the resource variable (e.g.,Scanner scanner;) before thetryblock. This ensures the variable is in scope and accessible within thefinallyblock, even if its initialization intryfails. - Cleanup, Not Business Logic: Avoid putting core business logic in the
finallyblock. Its sole purpose should be cleanup or ensuring state consistency. - Guaranteed Execution: Remember,
finallyalways runs. This makes it incredibly reliable for operations that absolutely must happen. The only exceptions are if the JVM exits early (e.g.,System.exit()) or a catastrophic error occurs.
4. Unsolved Exercise: Safe Array Access
Create a program that attempts to access an element of an array at a user-specified index.
- Create a class named
SafeArrayAccess. - Declare an
intarraydataand initialize it with 5 elements (e.g.,{1, 2, 3, 4, 5}). - Use a
Scannerto ask the user to enter anindex(integer). - Implement a
try-catch-finallyblock:- In the
tryblock, attempt to read theindexand then print the element atdata[index]. - Catch
InputMismatchExceptionif the user enters non-integer input. - Catch
ArrayIndexOutOfBoundsExceptionif theindexis invalid. - In the
finallyblock, ensure theScanneris closed and print a message indicating cleanup.
- In the
- After the
finallyblock, print "Program completed."
6. Complete Solution: Safe Array Access
// Chapter25/SafeArrayAccess.java
import java.util.InputMismatchException;
import java.util.Scanner;
public class SafeArrayAccess {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int[] data = {1, 2, 3, 4, 5}; // 5 elements, indices 0-4
int index = 0;
System.out.println("--- Safe Array Access ---");
System.out.println("Array elements: " + java.util.Arrays.toString(data));
System.out.print("Enter an index to access (0-" + (data.length - 1) + "): ");
try {
// Attempt to read the index
index = scanner.nextInt();
// Attempt to access and print the element
System.out.println("Element at index " + index + ": " + data[index]);
} catch (InputMismatchException e) {
// Handle non-integer input
System.err.println("Error: Invalid input! Please enter a whole number for the index.");
scanner.nextLine(); // Clear buffer
} catch (ArrayIndexOutOfBoundsException e) {
// Handle out-of-bounds index
System.err.println("Error: Index " + index + " is out of bounds for array length " + data.length + ".");
System.err.println("Valid indices are 0 to " + (data.length - 1) + ".");
} catch (Exception e) { // Catch any other unexpected exceptions
System.err.println("An unexpected error occurred: " + e.getMessage());
} finally {
// Ensure the scanner is always closed
System.out.println("--- Cleanup: Closing scanner. ---");
scanner.close();
}
System.out.println("Program completed.");
System.out.println("-------------------------");
}
}
Chapter 26: Custom Exceptions.
1. Quick Theory: Defining Your Own Errors
While Java provides many built-in exception types (like InputMismatchException or ArithmeticException), sometimes these aren't specific enough for the unique error conditions in your own applications. You might have business rules that, if violated, constitute an "error" in your program's logic, even if no standard Java exception applies.
This is where custom exceptions come in. You can define your own exception classes by extending Java's Exception class (for checked exceptions) or RuntimeException (for unchecked exceptions, which we'll use for simplicity here). Once defined, you can throw instances of your custom exceptions when a specific problem occurs in your code. This allows you to create more descriptive and context-specific error messages and types, improving the clarity and maintainability of your error handling. The "Why": Custom exceptions allow you to signal unique error conditions in a structured, consistent, and typesafe manner. Instead of returning error codes or printing generic messages, you create specific error types that can be caught and handled with precision, making your application's error behavior explicit and professional.
2. Professional Code: Throwing Your Own Errors
Example 26.1: Validating Age with a Custom Exception (Illustrative)
// Chapter26/AgeValidationException.java
// 1. Define a simple custom exception class
// By extending RuntimeException, it becomes an "unchecked" exception.
// This means methods that throw it are not *required* to declare it in their signature.
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message); // Call the constructor of the parent class (RuntimeException)
}
}
public class AgeValidationException {
public static void main(String[] args) {
System.out.println("--- Age Validation with Custom Exception ---");
// Test with a valid age
try {
validateAge(25);
System.out.println("Age 25 is valid.");
} catch (InvalidAgeException e) {
System.err.println("Error for age 25: " + e.getMessage());
}
// Test with an invalid age (too young)
try {
validateAge(5);
System.out.println("Age 5 is valid."); // This line won't be reached
} catch (InvalidAgeException e) {
System.err.println("Error for age 5: " + e.getMessage()); // Custom error caught
}
// Test with another invalid age (too old)
try {
validateAge(150);
System.out.println("Age 150 is valid."); // This line won't be reached
} catch (InvalidAgeException e) {
System.err.println("Error for age 150: " + e.getMessage()); // Custom error caught
}
System.out.println("Program continues after all validation attempts.");
System.out.println("------------------------------------------");
}
/**
* Validates if the given age is within a reasonable range (0-120).
* @param age The age to validate.
* @throws InvalidAgeException if the age is outside the valid range.
*/
public static void validateAge(int age) {
if (age < 0 || age > 120) {
// 2. Throwing an instance of our custom exception
throw new InvalidAgeException("Age must be between 0 and 120, but got: " + age);
}
System.out.println(" Validation successful for age: " + age);
}
}
Example 26.2: Throwing a Standard Exception for Validation
// Chapter26/OrderProcessor.java
public class OrderProcessor {
public static void main(String[] args) {
System.out.println("--- Order Processing Example ---");
// Test valid order
try {
processOrder("Laptop", 2);
System.out.println("Order for Laptop x2 processed.");
} catch (IllegalArgumentException e) {
System.err.println("Order error: " + e.getMessage());
}
// Test order with invalid quantity
try {
processOrder("Mouse", 0);
System.out.println("Order for Mouse x0 processed."); // This won't be reached
} catch (IllegalArgumentException e) {
System.err.println("Order error: " + e.getMessage()); // Catches our thrown exception
}
// Test order with negative quantity
try {
processOrder("Keyboard", -1);
System.out.println("Order for Keyboard x-1 processed."); // This won't be reached
} catch (IllegalArgumentException e) {
System.err.println("Order error: " + e.getMessage()); // Catches our thrown exception
}
System.out.println("Program finished order processing attempts.");
System.out.println("------------------------------");
}
/**
* Processes an order for a given item and quantity.
* Throws an IllegalArgumentException if the quantity is invalid.
* @param item The name of the item.
* @param quantity The quantity of the item.
* @throws IllegalArgumentException if quantity is less than or equal to 0.
*/
public static void processOrder(String item, int quantity) {
if (quantity <= 0) {
// Throw a standard Java exception (IllegalArgumentException)
// This is suitable when an argument passed to a method is invalid.
throw new IllegalArgumentException("Quantity must be greater than 0 for item: " + item + ", but got " + quantity);
}
// Simulate processing the order
System.out.println(" Processing " + quantity + " units of " + item + "...");
// More complex logic would go here
}
}
3. Pro-Tips: Judicious Error Throwing
- When to
throw: Throw an exception when a method encounters a situation it cannot gracefully handle itself, and it wants to signal to its caller that an unrecoverable error (for that method) has occurred. - Choose Wisely: Custom vs. Standard:
- Custom Exceptions: Use when you need to represent a specific, unique error condition related to your application's domain that isn't covered well by existing Java exceptions.
- Standard Exceptions: Often, a standard Java exception (like
IllegalArgumentExceptionfor invalid method arguments,IllegalStateExceptionfor an object in an incorrect state,NullPointerExceptionif a null reference is encountered unexpectedly) is sufficient and preferred for clarity. Don't create a custom exception if a standard one fits.
- Meaningful Messages: Always provide a clear and concise error message when throwing an exception. This message is invaluable for debugging.
- Runtime vs. Checked: For now, extend
RuntimeExceptionfor simplicity. Later, you'll learn about "checked" exceptions (extendingException), which must be declared in a method's signature or caught.
4. Unsolved Exercise: Product Stock Validator
You're writing a method to check if there's enough stock for a product.
- Create a class named
StockValidator. - Define a custom exception class
InsufficientStockExceptionthat extendsRuntimeException. It should have a constructor that takes aStringmessage. - In
StockValidator, implement apublic static voidmethodcheckStock(String productName, int availableStock, int requestedQuantity):- If
requestedQuantityis less than or equal to 0, throw anIllegalArgumentExceptionwith a descriptive message. - If
requestedQuantityis greater thanavailableStock, throw anInsufficientStockExceptionwith a message like "Not enough stock for [productName]. Available: [availableStock], Requested: [requestedQuantity]". - If stock is sufficient, print "Stock is sufficient for [productName].".
- If
- In the
mainmethod, testcheckStockwith three scenarios usingtry-catchblocks for each:- Valid request (e.g., "Keyboard", 10, 3)
- Invalid quantity (e.g., "Mouse", 5, 0)
- Insufficient stock (e.g., "Laptop", 2, 5)
6. Complete Solution: Product Stock Validator
// Chapter26/StockValidator.java
// 1. Define custom exception class
class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String message) {
super(message);
}
}
public class StockValidator {
public static void main(String[] args) {
System.out.println("--- Product Stock Validator ---");
// Scenario 1: Valid request
try {
checkStock("Keyboard", 10, 3);
} catch (IllegalArgumentException | InsufficientStockException e) {
System.err.println("Validation Error: " + e.getMessage());
}
System.out.println(); // New line for separation
// Scenario 2: Invalid quantity (zero)
try {
checkStock("Mouse", 5, 0);
} catch (IllegalArgumentException | InsufficientStockException e) {
System.err.println("Validation Error: " + e.getMessage());
}
System.out.println(); // New line for separation
// Scenario 3: Insufficient stock
try {
checkStock("Laptop", 2, 5);
} catch (IllegalArgumentException | InsufficientStockException e) {
System.err.println("Validation Error: " + e.getMessage());
}
System.out.println("\nAll stock checks completed.");
System.out.println("-----------------------------");
}
/**
* Checks if the requested quantity for a product can be fulfilled from available stock.
* Throws exceptions for invalid requests or insufficient stock.
* @param productName The name of the product.
* @param availableStock The current stock level.
* @param requestedQuantity The quantity requested by the user.
* @throws IllegalArgumentException if requestedQuantity is not positive.
* @throws InsufficientStockException if requestedQuantity exceeds availableStock.
*/
public static void checkStock(String productName, int availableStock, int requestedQuantity) {
System.out.println("Checking stock for " + productName + ": Available=" + availableStock + ", Requested=" + requestedQuantity);
if (requestedQuantity <= 0) {
throw new IllegalArgumentException("Requested quantity must be positive, but got: " + requestedQuantity);
}
if (requestedQuantity > availableStock) {
throw new InsufficientStockException("Not enough stock for " + productName + ". Available: " + availableStock + ", Requested: " + requestedQuantity);
}
System.out.println(" Stock is sufficient for " + productName + ". " + (availableStock - requestedQuantity) + " left.");
}
}
Chapter 27: Writing to Files.
1. Quick Theory: Saving Your Program's Work
So far, all data in your programs (variables, arrays) exists only while the program is running. Once the program terminates, that data is lost. This is not useful for real-world applications! Data persistence is the ability of data to outlive the process that created it. The most common and fundamental way to achieve this is by writing data to files.
When writing to files in Java, you typically use FileWriter or FileOutputStream to establish a connection to the file, and then PrintWriter or BufferedWriter to provide convenient methods for writing text (like println(), print(), printf()). File operations are inherently risky: the file might not exist, you might not have permission to write, or the disk might be full. These issues throw IOExceptions, which are "checked exceptions" (meaning you must explicitly try-catch them or declare them with throws in your method signature). The "Why": Writing to files is essential for saving user data, configurations, logs, reports, and any other information your program needs to store long-term. Mastering file writing is your first step towards building applications that can save and retrieve their state, moving beyond ephemeral runtime data.
2. Professional Code: Storing Data in Files
Example 27.1: Writing Simple Text to a File
// Chapter27/SimpleFileWriter.java
import java.io.FileWriter; // Needed for writing character data to a file
import java.io.IOException; // Needed for handling potential file operation errors
import java.io.PrintWriter; // Provides convenient methods like println()
public class SimpleFileWriter {
public static void main(String[] args) {
// Define the name of the file we want to write to
String fileName = "output_log.txt";
// Declare FileWriter and PrintWriter outside the try block
// so they can be accessed in the finally block for closing.
FileWriter fileWriter = null;
PrintWriter printWriter = null;
System.out.println("--- Writing Simple Text to File ---");
try {
// 1. Create a FileWriter object.
// The 'false' argument (default) means overwrite if file exists.
// Using 'true' as second argument would append to the file.
fileWriter = new FileWriter(fileName, false); // Overwrite mode
// 2. Wrap the FileWriter in a PrintWriter for easier writing (like System.out.println)
printWriter = new PrintWriter(fileWriter);
// 3. Write data to the file
printWriter.println("This is the first line of my log file.");
printWriter.println("Java file writing is powerful.");
printWriter.printf("Current value of PI is %.5f.\n", Math.PI);
printWriter.print("This is without a newline."); // Use print() for no newline
System.out.println("Data successfully written to " + fileName);
} catch (IOException e) {
// Catch and handle potential I/O errors (e.g., permission denied, disk full)
System.err.println("An I/O error occurred while writing to file: " + e.getMessage());
} finally {
// 4. IMPORTANT: Always close the writers in a finally block
// to release system resources and ensure data is flushed to disk.
// Close PrintWriter first, then FileWriter.
if (printWriter != null) {
printWriter.close(); // Closes the underlying FileWriter too if not explicitly closed
}
// if (fileWriter != null) { // This is generally not needed if PrintWriter is closing FileWriter
// try {
// fileWriter.close();
// } catch (IOException e) {
// System.err.println("Error closing FileWriter: " + e.getMessage());
// }
// }
System.out.println("File writers closed.");
}
System.out.println("-----------------------------------");
}
}
Example 27.2: Appending Data and Saving from an Array
// Chapter27/AppendToFileFromArray.java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class AppendToFileFromArray {
public static void main(String[] args) {
String fileName = "names_and_scores.txt";
String[] studentNames = {"Alice", "Bob", "Charlie", "Diana"};
int[] scores = {85, 92, 78, 95};
FileWriter fileWriter = null;
PrintWriter printWriter = null;
System.out.println("--- Appending Data to File from Array ---");
try {
// Create FileWriter in append mode (true as second argument)
fileWriter = new FileWriter(fileName, true); // Append mode
// Wrap in PrintWriter
printWriter = new PrintWriter(fileWriter);
// Write a header or separator for clarity in append mode
printWriter.println("\n--- New Data Entry ---");
// Write data from arrays
for (int i = 0; i < studentNames.length; i++) {
printWriter.println("Name: " + studentNames[i] + ", Score: " + scores[i]);
}
System.out.println("Data successfully appended to " + fileName);
} catch (IOException e) {
System.err.println("An I/O error occurred while appending to file: " + e.getMessage());
} finally {
if (printWriter != null) {
printWriter.close();
}
System.out.println("File writers closed.");
}
System.out.println("-----------------------------------------");
}
}
3. Pro-Tips: Persistent Data Best Practices
- Handle
IOException: File operations are inherently risky. Always wrap your file writing code in atry-catchblock to gracefully handleIOException. - Close Resources: This is critical. Always close your
FileWriterandPrintWriterobjects in afinallyblock to prevent resource leaks and ensure all buffered data is written to the file. Not closing them can lead to empty or incomplete files. - Append vs. Overwrite: Understand the second argument in the
FileWriterconstructor:false(or omitting it) overwrites the file;trueappends to the end of the file. - Meaningful File Names: Choose clear and descriptive names for your files (e.g.,
user_data.csv,application_log.txt).
4. Unsolved Exercise: Employee Records
You need to save employee details to a file.
- Create a class named
EmployeeRecordsWriter. - Declare two
Stringarrays:employeeNames(e.g.,"John Doe", "Jane Smith") andemployeeIDs(e.g.,"E001", "E002"). - Define a file name, e.g.,
"employees.csv". - Write a program that uses
FileWriterandPrintWriterto write each employee's name and ID to the file, with each entry on a new line, separated by a comma. (e.g.,John Doe,E001). Overwrite the file if it exists. - Ensure proper
try-catchforIOExceptionandfinallyfor closing resources.
6. Complete Solution: Employee Records
// Chapter27/EmployeeRecordsWriter.java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class EmployeeRecordsWriter {
public static void main(String[] args) {
// 2. Declare employee data arrays
String[] employeeNames = {"John Doe", "Jane Smith", "Peter Jones"};
String[] employeeIDs = {"E001", "E002", "E003"};
// 3. Define the file name
String fileName = "employees.csv";
FileWriter fileWriter = null;
PrintWriter printWriter = null;
System.out.println("--- Writing Employee Records to File ---");
try {
// 4. Create FileWriter (overwrite mode) and PrintWriter
fileWriter = new FileWriter(fileName, false); // Overwrite mode
printWriter = new PrintWriter(fileWriter);
// Write a header line
printWriter.println("Name,ID");
// Loop through employee data and write to file
for (int i = 0; i < employeeNames.length; i++) {
printWriter.println(employeeNames[i] + "," + employeeIDs[i]);
}
System.out.println("Employee records successfully written to " + fileName);
} catch (IOException e) {
System.err.println("An I/O error occurred: " + e.getMessage());
} finally {
// 5. Ensure resources are closed
if (printWriter != null) {
printWriter.close();
}
System.out.println("File writers closed.");
}
System.out.println("----------------------------------------");
}
}
Chapter 28: Reading from Files.
1. Quick Theory: Loading Your Stored Data
Once you've saved data to a file, the next crucial step is to retrieve it. Reading from files allows your programs to load previously saved information, resume state, or process external datasets. This makes your applications truly dynamic and capable of interacting with the stored world.
To read text files in Java, you typically use the File class to represent the physical file on the disk, and then the Scanner class (which you're already familiar with for console input) linked to that File object. Just like writing, file reading operations are susceptible to IOExceptions (e.g., the file doesn't exist, you lack read permissions). A common specific IOException for reading is FileNotFoundException. You'll often use a while(scanner.hasNextLine()) loop to read the file line by line until the end is reached. The "Why": Reading from files is the other half of data persistence. Without it, saved data would be useless. It enables your programs to be configured, to process logs, to display past user actions, and generally to leverage data that outlives a single program run.
2. Professional Code: Retrieving Data from Files
Example 28.1: Reading a File Line by Line
// Chapter28/SimpleFileReader.java
import java.io.File; // Represents the file in the file system
import java.io.FileNotFoundException; // Specific exception for when file doesn't exist
import java.util.Scanner; // Used to read from the file
public class SimpleFileReader {
public static void main(String[] args) {
String fileName = "output_log.txt"; // Assuming this file was created by Example 27.1
File file = new File(fileName); // Create a File object representing the file
Scanner fileScanner = null; // Declare Scanner outside try block for finally
System.out.println("--- Reading File Line by Line ---");
try {
// 1. Create a Scanner object, linking it to our File object
fileScanner = new Scanner(file);
System.out.println("Contents of " + fileName + ":");
// 2. Loop through the file as long as there are more lines
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine(); // Read the entire next line
System.out.println(" " + line); // Print the line to console
}
System.out.println("\nSuccessfully read all lines from " + fileName);
} catch (FileNotFoundException e) {
// This specific exception is caught if the file does not exist at the specified path
System.err.println("Error: File not found at '" + fileName + "'. " + e.getMessage());
System.err.println("Please ensure the file exists in the correct directory (e.g., project root).");
} catch (Exception e) { // Catch any other unexpected exceptions during file reading
System.err.println("An unexpected error occurred while reading file: " + e.getMessage());
} finally {
// 3. IMPORTANT: Always close the Scanner in a finally block
// to release system resources.
if (fileScanner != null) {
fileScanner.close();
}
System.out.println("File scanner closed.");
}
System.out.println("-------------------------------");
}
}
Example 28.2: Reading and Processing Delimited Data from File
// Chapter28/StudentDataReader.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.InputMismatchException;
import java.util.Scanner;
public class StudentDataReader {
public static void main(String[] args) {
// Assume 'grades.txt' contains lines like "Alice,95", "Bob,88", etc.
// It's good practice to create this file manually or via a separate program for testing.
String fileName = "student_grades.txt";
// Example content for student_grades.txt:
// Alice,95
// Bob,88
// Charlie,72
// Diana,invalid_score // This will cause an InputMismatchException
File file = new File(fileName);
Scanner fileScanner = null;
int totalValidScores = 0;
int validStudentCount = 0;
System.out.println("--- Reading Student Data from File ---");
try {
fileScanner = new Scanner(file);
System.out.println("Processing student grades:");
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine();
// Use String.split() to parse the line (assuming comma-separated: Name,Score)
String[] parts = line.split(",");
if (parts.length == 2) {
String name = parts[0].trim(); // Trim whitespace from name
try {
int score = Integer.parseInt(parts[1].trim()); // Parse score, trim whitespace
totalValidScores += score; // Accumulate score
validStudentCount++; // Count valid students
System.out.println(" Processed: " + name + " (Score: " + score + ")");
} catch (NumberFormatException e) {
System.err.println(" Error: Invalid score format for '" + name + "'. Skipping line: " + line);
}
} else {
System.err.println(" Error: Invalid line format. Expected 'Name,Score'. Skipping line: " + line);
}
}
if (validStudentCount > 0) {
double averageScore = (double) totalValidScores / validStudentCount;
System.out.printf("\nTotal valid students: %d, Average score: %.2f\n", validStudentCount, averageScore);
} else {
System.out.println("\nNo valid student data found or processed.");
}
} catch (FileNotFoundException e) {
System.err.println("Error: Student grades file '" + fileName + "' not found. " + e.getMessage());
} catch (Exception e) {
System.err.println("An unexpected error occurred: " + e.getMessage());
} finally {
if (fileScanner != null) {
fileScanner.close();
}
System.out.println("File scanner closed.");
}
System.out.println("--------------------------------------");
}
}
3. Pro-Tips: Robust File Reading
- Handle
FileNotFoundException: This is the most common error when reading files. Always explicitly catch it. - Check
hasNextLine(): Usewhile (scanner.hasNextLine())as your loop condition to ensure you don't try to read past the end of the file. - Parse Carefully: When reading delimited data (like CSV), use
String.split()and handle potentialNumberFormatException(if parsing numbers) orArrayIndexOutOfBoundsException(if lines don't have the expected number of parts). - Trim Data: Data read from files often contains leading/trailing whitespace. Use
String.trim()to clean it up before processing. - Close Resources: Just like writing, always close your
Scanner(and any underlyingFileReader) in afinallyblock to release file handles.
4. Unsolved Exercise: Read Employee Records
You need to read the employees.csv file created in the previous exercise and display its contents.
- Create a class named
EmployeeRecordsReader. - Define the file name
employees.csv. - Use a
Fileobject and aScannerto read the file line by line. - Print each line to the console.
- Ensure
FileNotFoundExceptionis caught and theScanneris closed in afinallyblock.
6. Complete Solution: Read Employee Records
// Chapter28/EmployeeRecordsReader.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class EmployeeRecordsReader {
public static void main(String[] args) {
// 2. Define the file name
String fileName = "employees.csv"; // Assuming this file was created by EmployeeRecordsWriter
File employeeFile = new File(fileName);
Scanner fileScanner = null;
System.out.println("--- Reading Employee Records from File ---");
try {
// 3. Create Scanner linked to the File object
fileScanner = new Scanner(employeeFile);
System.out.println("Contents of " + fileName + ":");
// 4. Read and print each line
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine();
System.out.println(" " + line);
}
System.out.println("\nSuccessfully read all employee records.");
} catch (FileNotFoundException e) {
// 5. Catch FileNotFoundException
System.err.println("Error: Employee records file '" + fileName + "' not found. " + e.getMessage());
System.err.println("Please ensure the file exists in the same directory as the program.");
} finally {
// 5. Ensure Scanner is closed
if (fileScanner != null) {
fileScanner.close();
}
System.out.println("File scanner closed.");
}
System.out.println("----------------------------------------");
}
}
Chapter 29: The 'Try-with-Resources' Pattern.
1. Quick Theory: Automatic Resource Management
We've learned that closing resources (like Scanner, FileWriter, PrintWriter) is absolutely critical to prevent leaks and ensure data integrity. The finally block helps, but it can still be verbose, especially if you have multiple resources that need closing, each requiring its own null check and try-catch for the close() method. This is a common boilerplate code.
Java 7 introduced the try-with-resources statement to elegantly solve this problem. It's a special try statement that declares one or more "resources" (objects that implement the java.lang.AutoCloseable interface) within its parentheses. At the end of the try block (whether it completes normally or an exception is thrown), Java automatically and implicitly calls the close() method on these declared resources. The "Why": try-with-resources is the professional, modern way to handle resources in Java. It drastically reduces boilerplate code, improves readability, and, most importantly, makes your programs more robust by guaranteeing that resources are always closed, even if you forget or if an unexpected exception occurs. This is a crucial feature for writing bulletproof applications.
2. Professional Code: Streamlined Resource Handling
Example 29.1: Writing to File with Try-with-Resources
// Chapter29/FileWriterWithResources.java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class FileWriterWithResources {
public static void main(String[] args) {
String fileName = "auto_closed_log.txt";
System.out.println("--- Writing to File with Try-with-Resources ---");
// The resources (FileWriter and PrintWriter) are declared inside the try parentheses.
// They will be automatically closed by Java when the try block exits,
// regardless of success or failure.
try (FileWriter fileWriter = new FileWriter(fileName, false); // Overwrite mode
PrintWriter printWriter = new PrintWriter(fileWriter)) {
printWriter.println("This log entry was written using try-with-resources.");
printWriter.println("It's much cleaner and safer!");
printWriter.printf("Random number: %.2f\n", Math.random());
System.out.println("Data successfully written to " + fileName);
} catch (IOException e) {
// Only need to catch exceptions that might occur *during* writing or resource creation.
System.err.println("An I/O error occurred: " + e.getMessage());
}
// No 'finally' block needed for closing resources, Java handles it!
// You would still use 'finally' if you had other cleanup logic not related to AutoCloseable resources.
System.out.println("Program continues. Resources are guaranteed to be closed.");
System.out.println("-----------------------------------------------");
}
}
Example 29.2: Reading from File with Try-with-Resources
// Chapter29/FileReaderWithResources.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class FileReaderWithResources {
public static void main(String[] args) {
String fileName = "auto_closed_log.txt"; // Use the file from the previous example
System.out.println("--- Reading from File with Try-with-Resources ---");
// Declare the Scanner (which is AutoCloseable) within the try-with-resources statement.
try (Scanner fileScanner = new Scanner(new File(fileName))) {
System.out.println("Contents of " + fileName + ":");
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine();
System.out.println(" " + line);
}
System.out.println("\nSuccessfully read all lines from " + fileName);
} catch (FileNotFoundException e) {
// Catch specific exception for file not found
System.err.println("Error: File '" + fileName + "' not found. " + e.getMessage());
} catch (Exception e) {
// Catch any other general exceptions during reading
System.err.println("An unexpected error occurred: " + e.getMessage());
}
// No 'finally' block needed for closing fileScanner!
System.out.println("Program continues. File scanner is guaranteed to be closed.");
System.out.println("---------------------------------------------");
}
}
3. Pro-Tips: Embrace Try-with-Resources
- Always Use It: For any object that implements
AutoCloseable(likeScanner,FileWriter,PrintWriter,FileInputStream,FileOutputStream,BufferedReader,BufferedWriter, database connections, etc.), always usetry-with-resources. It's safer, cleaner, and less error-prone than manualfinallyblocks for closing. - Multiple Resources: You can declare multiple resources in a single
try-with-resourcesstatement, separated by semicolons (as seen inFileWriterWithResources.java). They will be closed in the reverse order of their declaration. - No Explicit
close(): You do not need to (and should not) callclose()explicitly on resources declared withintry-with-resources. Java handles it. - Error Handling Still Needed: While
try-with-resourceshandles closing, you still needcatchblocks to handle any exceptions that might occur during the creation of the resources or during the operations within thetryblock.
4. Unsolved Exercise: Config File Management
Rewrite the EmployeeRecordsWriter and EmployeeRecordsReader exercises from Chapters 27 and 28 to use the try-with-resources pattern.
- Create a class named
ConfigFileManager. - Inside
main, first, write the employee data toemployees.csvusingtry-with-resources. - Then, read and print the
employees.csvdata using anothertry-with-resourcesblock. - Ensure all necessary exceptions are caught for both operations.
6. Complete Solution: Config File Management
// Chapter29/ConfigFileManager.java
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
public class ConfigFileManager {
public static void main(String[] args) {
String fileName = "employees.csv";
String[] employeeNames = {"John Doe", "Jane Smith", "Peter Jones", "Alice Wonderland"};
String[] employeeIDs = {"E001", "E002", "E003", "E004"};
System.out.println("--- Config File Manager ---");
// Part 1: Writing employee data to file using try-with-resources
System.out.println("Attempting to write employee data to " + fileName + "...");
try (FileWriter fileWriter = new FileWriter(fileName, false); // Overwrite mode
PrintWriter printWriter = new PrintWriter(fileWriter)) {
printWriter.println("Name,ID"); // Header
for (int i = 0; i < employeeNames.length; i++) {
printWriter.println(employeeNames[i] + "," + employeeIDs[i]);
}
System.out.println(" Employee data successfully written.");
} catch (IOException e) {
System.err.println(" Error writing employee data: " + e.getMessage());
}
System.out.println("\n--- Now Reading Employee Data ---");
// Part 2: Reading employee data from file using try-with-resources
try (Scanner fileScanner = new Scanner(new File(fileName))) {
System.out.println("Contents of " + fileName + ":");
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine();
System.out.println(" " + line);
}
System.out.println(" Employee data successfully read.");
} catch (FileNotFoundException e) {
System.err.println(" Error: File '" + fileName + "' not found. " + e.getMessage());
} catch (Exception e) { // Catch any other general exceptions during reading
System.err.println(" An unexpected error occurred while reading file: " + e.getMessage());
}
System.out.println("\n--- Operations Complete ---");
System.out.println("All file resources are guaranteed to be closed.");
System.out.println("---------------------------");
}
}
2º Java Book - OOP Foundations
Chapter 1: Classes vs Objects
Quick Theory: The Object-Oriented mindset is about modeling the real world using software constructs. Instead of thinking about procedures or steps, we think about "things" or "entities" and how they behave and interact. This approach often leads to more maintainable, scalable, and understandable code by organizing complexity around coherent units.
At the core of OOP are Classes and Objects. Think of a Class as a blueprint or a template for creating something – for example, a blueprint for a house. It defines what attributes (like number of rooms, color) and behaviors (like open door, turn on lights) that thing will have. An Object, on the other hand, is a concrete instance built from that blueprint – an actual house standing on a street. You can build many houses from one blueprint, and each house (object) will be distinct, even if they share the same structure.
Professional Code:
// Example 1: Defining a simple 'Person' class (the blueprint)
// A class is a template for creating objects.
class Person {
// Attributes (state) - these describe the characteristics of a Person.
String name;
int age;
// We'll add methods (behavior) in a later chapter.
}
// Example 2: Instantiating a 'Person' object in the main method
public class ObjectCreationDemo {
public static void main(String[] args) {
// Here, we're using the 'Person' blueprint to create an actual 'Person' object.
// The 'new' keyword is used to allocate memory for a new object.
// 'person1' is a reference variable that holds the memory address of the new Person object.
Person person1 = new Person();
// Now we can access and set the attributes of this specific 'person1' object.
person1.name = "Alice";
person1.age = 30;
// Printing the details of the object.
// By default, printing an object will show its class name and a hash code.
// We'll learn how to make this output more meaningful in Chapter 5.
System.out.println("Person 1 object reference: " + person1);
System.out.println("Person 1 Name: " + person1.name);
System.out.println("Person 1 Age: " + person1.age);
}
}
// Example 3: Creating multiple distinct objects from the same class
public class MultipleObjectsDemo {
public static void main(String[] args) {
// Create the first Person object.
Person personA = new Person();
personA.name = "Bob";
personA.age = 25;
// Create a second, completely distinct Person object from the same 'Person' blueprint.
Person personB = new Person();
personB.name = "Charlie";
personB.age = 35;
// Notice that personA and personB are independent instances.
// Changes to one do not affect the other.
System.out.println("--- Details for Person A ---");
System.out.println("Name: " + personA.name);
System.out.println("Age: " + personA.age);
System.out.println("Object reference: " + personA); // Different memory address
System.out.println("\n--- Details for Person B ---");
System.out.println("Name: " + personB.name);
System.out.println("Age: " + personB.age);
System.out.println("Object reference: " + personB); // Different memory address
}
}
Clean Code Tip:
When designing a class, strive for a single responsibility. A class should have one, and only one, reason to change. This is the "S" in the SOLID principles (Single Responsibility Principle). For instance, a Person class should manage person-related data and behavior, not database persistence or UI display.
Exercise:
Create a simple class named Book. It should have two attributes: title (String) and author (String). In a main method, create two Book objects, set their title and author, and then print out the details of each book.
Solution:
// Define the Book class
class Book {
String title;
String author;
}
public class BookExerciseSolution {
public static void main(String[] args) {
// Create the first Book object
Book book1 = new Book();
book1.title = "The Hitchhiker's Guide to the Galaxy";
book1.author = "Douglas Adams";
// Create the second Book object
Book book2 = new Book();
book2.title = "1984";
book2.author = "George Orwell";
// Print details for book1
System.out.println("--- Book 1 Details ---");
System.out.println("Title: " + book1.title);
System.out.println("Author: " + book1.author);
// Print details for book2
System.out.println("\n--- Book 2 Details ---");
System.out.println("Title: " + book2.title);
System.out.println("Author: " + book2.author);
}
}
Chapter 2: Attributes & Methods
Quick Theory: In Object-Oriented Programming, objects encapsulate both data (state) and the operations that can be performed on that data (behavior). Attributes, also known as fields or member variables, define the state of an object—what it is or what it has. Methods, on the other hand, define the behavior of an object—what it does or what can be done to it. Together, attributes and methods give objects their complete functionality and identity.
Objects interact by calling each other's methods. This interaction is the cornerstone of object-oriented design, allowing complex systems to be built from smaller, manageable, and interconnected components. For example, a Driver object might interact with a Car object by calling its startEngine() or accelerate() methods, modifying the car's internal state (like speed) and triggering its behaviors.
Professional Code:
// Example 1: Car class with attributes (state) and methods (behavior)
class Car {
// Attributes - represent the state of a Car object.
String make;
String model;
int year;
int speed; // Current speed of the car, initialized to 0 by default.
// Methods - represent the behavior of a Car object.
// Method to start the car's engine.
void startEngine() {
System.out.println(make + " " + model + "'s engine started.");
// Starting the engine might implicitly set a state like 'isRunning = true',
// but for simplicity, we'll just print a message for now.
}
// Method to accelerate the car.
// It takes an 'amount' as a parameter to increase the speed.
void accelerate(int amount) {
if (amount > 0) {
speed += amount; // Increase the car's speed.
System.out.println(make + " " + model + " is accelerating. Current speed: " + speed + " km/h.");
} else {
System.out.println("Acceleration amount must be positive.");
}
}
// Method to brake the car.
void brake(int amount) {
if (amount > 0 && speed - amount >= 0) {
speed -= amount; // Decrease the car's speed.
System.out.println(make + " " + model + " is braking. Current speed: " + speed + " km/h.");
} else if (speed - amount < 0) {
speed = 0; // Cannot have negative speed.
System.out.println(make + " " + model + " has stopped. Current speed: " + speed + " km/h.");
} else {
System.out.println("Brake amount must be positive.");
}
}
// Method to display the current status of the car.
void displayStatus() {
System.out.println("Car: " + make + " " + model + " (" + year + ")");
System.out.println("Current Speed: " + speed + " km/h.");
}
}
// Example 2: Creating a Car object and calling its methods
public class CarOperationsDemo {
public static void main(String[] args) {
// Create a new Car object.
Car myCar = new Car();
// Set its attributes (state).
myCar.make = "Toyota";
myCar.model = "Camry";
myCar.year = 2022;
// Call its methods (behavior).
myCar.displayStatus(); // Show initial status.
myCar.startEngine(); // Start the engine.
myCar.accelerate(50); // Accelerate by 50 km/h.
myCar.accelerate(20); // Accelerate by another 20 km/h.
myCar.brake(30); // Brake by 30 km/h.
myCar.displayStatus(); // Show updated status.
myCar.brake(100); // Brake beyond current speed to stop.
myCar.displayStatus();
}
}
// Example 3: Objects interacting with each other (a simple scenario)
class Driver {
String name;
// Constructor to initialize the driver's name (we'll cover constructors in Chapter 3).
Driver(String name) {
this.name = name;
}
// A driver's behavior: driving a car.
void driveCar(Car carToDrive, int accelerationAmount, int brakeAmount) {
System.out.println("\n" + name + " is now driving the " + carToDrive.make + " " + carToDrive.model + ".");
carToDrive.startEngine();
carToDrive.accelerate(accelerationAmount);
carToDrive.brake(brakeAmount);
carToDrive.displayStatus();
}
}
public class ObjectInteractionDemo {
public static void main(String[] args) {
// Create a Car object.
Car sedan = new Car();
sedan.make = "Honda";
sedan.model = "Civic";
sedan.year = 2023;
// Create a Driver object.
Driver john = new Driver("John");
// The Driver object interacts with the Car object by calling its methods.
john.driveCar(sedan, 60, 20);
// Another driver, another car.
Car suv = new Car();
suv.make = "Ford";
suv.model = "Explorer";
suv.year = 2024;
Driver jane = new Driver("Jane");
jane.driveCar(suv, 80, 40);
}
}
Clean Code Tip:
Name methods clearly using verbs that describe the action they perform (e.g., startEngine(), calculateArea(), processOrder()). Name attributes using nouns that describe the state they hold (e.g., make, model, speed). This makes the code self-documenting and easier to understand, reflecting the natural language of the problem domain.
Exercise:
Enhance your Dog class from Chapter 1. Add attributes for name (String) and breed (String). Then, add a method called bark() that prints "[Dog's Name] says Woof! Woof!" and a method called displayInfo() that prints the dog's name and breed. In your main method, create a Dog object, set its attributes, and call its methods.
Solution:
class Dog {
String name;
String breed;
// We can also add an 'age' attribute to follow the example.
int age;
// Method to make the dog bark
void bark() {
System.out.println(name + " says Woof! Woof!");
}
// Method to display the dog's information
void displayInfo() {
System.out.println("Dog Name: " + name);
System.out.println("Breed: " + breed);
System.out.println("Age: " + age + " years");
}
}
public class DogExerciseSolution {
public static void main(String[] args) {
// Create a Dog object
Dog myDog = new Dog();
// Set its attributes
myDog.name = "Buddy";
myDog.breed = "Golden Retriever";
myDog.age = 5;
// Call its methods
myDog.displayInfo();
myDog.bark();
// Create another Dog object
Dog neighborDog = new Dog();
neighborDog.name = "Max";
neighborDog.breed = "German Shepherd";
neighborDog.age = 3;
neighborDog.displayInfo();
neighborDog.bark();
}
}
Chapter 3: Constructors
Quick Theory:
Constructors are special methods used to initialize objects. When you create an object using the new keyword (e.g., new Person()), a constructor is invoked. Their primary purpose is to ensure that a newly created object is in a valid and usable state right from its inception. If you don't define any constructor in your class, Java provides a default, no-argument constructor implicitly. However, once you define any constructor, Java no longer provides the default one.
Constructors can be "no-arg" (taking no arguments) or "parameterized" (taking one or more arguments). Parameterized constructors are very common as they allow you to set the initial state of an object with specific values at the time of its creation. The this keyword is crucial within a constructor (or any instance method) to refer to the current object itself, particularly useful for distinguishing between an instance variable and a local parameter with the same name.
Professional Code:
// Example 1: Default (implicit) and No-Arg (explicit) Constructors
class Product {
String name;
double price;
// If you don't define any constructor, Java provides a default no-arg constructor.
// However, if you define *any* constructor, Java doesn't provide the default one.
// It's good practice to explicitly define a no-arg constructor if you need one,
// especially if you also define parameterized constructors.
// Explicit No-Arg Constructor
// It takes no arguments and typically sets default values or performs basic initialization.
public Product() {
this.name = "Unknown Product"; // Initialize with a default name.
this.price = 0.0; // Initialize with a default price.
System.out.println("Product created using no-arg constructor.");
}
}
// Example 2: Parameterized Constructor and the 'this' keyword
class OrderItem {
int itemId;
String description;
int quantity;
double unitPrice;
// Parameterized Constructor
// This constructor takes arguments to initialize the object's state upon creation.
// The 'this' keyword is used to differentiate between the instance variable (e.g., this.itemId)
// and the local parameter (e.g., itemId).
public OrderItem(int itemId, String description, int quantity, double unitPrice) {
this.itemId = itemId; // Assign parameter 'itemId' to the instance variable 'this.itemId'.
this.description = description; // Assign parameter 'description' to 'this.description'.
this.quantity = quantity; // Assign parameter 'quantity' to 'this.quantity'.
this.unitPrice = unitPrice; // Assign parameter 'unitPrice' to 'this.unitPrice'.
System.out.println("OrderItem created: " + description + " (Qty: " + quantity + ").");
}
// Method to calculate the total cost for this order item.
public double calculateTotal() {
return quantity * unitPrice;
}
// A no-arg constructor can also be provided alongside parameterized ones.
public OrderItem() {
this(0, "Default Item", 1, 0.0); // Chaining to the parameterized constructor using 'this()'
System.out.println("OrderItem created using no-arg constructor (defaulted).");
}
}
// Example 3: Demonstrating different constructors in Main method
public class ConstructorDemo {
public static void main(String[] args) {
// Using the no-arg constructor for Product
Product p1 = new Product();
System.out.println("Product 1 Name: " + p1.name + ", Price: " + p1.price); // Shows default values
// Creating an OrderItem using the parameterized constructor
OrderItem item1 = new OrderItem(101, "Laptop", 1, 1200.00);
System.out.println("Item 1 Total: $" + item1.calculateTotal());
// Creating another OrderItem using the parameterized constructor
OrderItem item2 = new OrderItem(102, "Mouse", 2, 25.50);
System.out.println("Item 2 Total: $" + item2.calculateTotal());
// Creating an OrderItem using the no-arg constructor (which chains to the parameterized one)
OrderItem item3 = new OrderItem();
System.out.println("Item 3 Description: " + item3.description + ", Total: $" + item3.calculateTotal());
// We could also create a Product and set its values manually, but constructors are for initial setup.
Product p2 = new Product();
p2.name = "Coffee Mug";
p2.price = 15.99;
System.out.println("Product 2 Name: " + p2.name + ", Price: " + p2.price);
}
}
Clean Code Tip: Always provide constructors that ensure an object is created in a valid and consistent state. Avoid creating objects that require multiple subsequent calls to setters to become usable. If an object must have certain attributes to be valid, then a parameterized constructor requiring those attributes is appropriate.
Exercise:
Modify your Dog class. Add:
- A no-argument constructor that sets a default
name(e.g., "Unnamed") andbreed(e.g., "Mixed"). - A parameterized constructor that takes
name,breed, andageas arguments to initialize the dog. In yourmainmethod, create oneDogobject using the no-arg constructor and another using the parameterized constructor. CalldisplayInfo()for both.
Solution:
class Dog {
String name;
String breed;
int age;
// 1. No-arg constructor
public Dog() {
this.name = "Unnamed";
this.breed = "Mixed";
this.age = 0; // Default age
System.out.println("No-arg Dog constructor called.");
}
// 2. Parameterized constructor
public Dog(String name, String breed, int age) {
this.name = name;
this.breed = breed;
this.age = age;
System.out.println("Parameterized Dog constructor called for " + name + ".");
}
void bark() {
System.out.println(name + " says Woof! Woof!");
}
void displayInfo() {
System.out.println("--- Dog Info ---");
System.out.println("Name: " + name);
System.out.println("Breed: " + breed);
System.out.println("Age: " + age + " years");
}
}
public class DogConstructorExerciseSolution {
public static void main(String[] args) {
// Create a Dog object using the no-arg constructor
Dog defaultDog = new Dog();
defaultDog.displayInfo();
defaultDog.bark();
System.out.println("\n------------------\n");
// Create a Dog object using the parameterized constructor
Dog namedDog = new Dog("Luna", "Siberian Husky", 2);
namedDog.displayInfo();
namedDog.bark();
System.out.println("\n------------------\n");
// We can still modify the default dog's attributes after creation if needed
defaultDog.name = "Pudding";
defaultDog.breed = "Poodle";
defaultDog.age = 7;
defaultDog.displayInfo();
}
}
Chapter 4: Encapsulation (Access Modifiers)
Quick Theory: Encapsulation is one of the fundamental principles of Object-Oriented Programming. It's the mechanism of bundling data (attributes) and the methods (behaviors) that operate on that data within a single unit, which is typically a class. More importantly, encapsulation involves restricting direct access to some of an object's components, meaning that internal state is hidden from the outside world. This "data hiding" ensures that the object's internal state can only be accessed or modified in controlled ways, usually through public methods.
The power of private and public access modifiers comes into play here. private members (attributes or methods) are only accessible from within the class itself, providing a protective barrier. public members are accessible from anywhere, forming the "interface" that other classes use to interact with the object. By hiding data (private) and exposing controlled access through public methods (Getters to read, Setters to write), we maintain the integrity of the object's state, prevent misuse, and allow internal implementation details to change without affecting external code that uses the class.
Professional Code:
// Example 1: BankAccount with private balance and public methods (Deposit/Withdraw)
class BankAccount {
// Attributes should ideally be private to protect the internal state.
// This prevents external code from directly manipulating 'balance'.
private String accountNumber;
private String accountHolder;
private double balance; // This is the sensitive data we want to protect.
// Constructor to initialize a BankAccount object.
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
// Perform validation during initialization.
if (initialBalance >= 0) {
this.balance = initialBalance;
} else {
System.err.println("Initial balance cannot be negative. Setting to 0.");
this.balance = 0;
}
}
// Public method to deposit money. This is a controlled way to change 'balance'.
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
System.out.println("Deposited $" + amount + ". New balance: $" + this.balance);
} else {
System.err.println("Deposit amount must be positive.");
}
}
// Public method to withdraw money. This is another controlled way to change 'balance'.
public void withdraw(double amount) {
if (amount > 0 && this.balance >= amount) {
this.balance -= amount;
System.out.println("Withdrew $" + amount + ". New balance: $" + this.balance);
} else if (amount <= 0) {
System.err.println("Withdrawal amount must be positive.");
} else {
System.err.println("Insufficient funds. Current balance: $" + this.balance);
}
}
// Public method to get the current balance. This is the only way to read 'balance' from outside.
public double getBalance() {
return this.balance;
}
// Getters for other private attributes.
public String getAccountNumber() {
return accountNumber;
}
public String getAccountHolder() {
return accountHolder;
}
}
// Example 2: Using Getters and Setters for other attributes
// This demonstrates how to expose read/write access to private fields in a controlled manner.
class UserProfile {
private String username;
private String email;
private int age; // Assume age must be positive
public UserProfile(String username, String email, int age) {
this.username = username;
// Basic validation in constructor
setEmail(email); // Use the setter to apply validation
setAge(age); // Use the setter to apply validation
}
// Getter for username (read-only access)
public String getUsername() {
return username;
}
// Setter for username (if we want to allow modification after creation)
public void setUsername(String username) {
// We could add validation here, e.g., check for length, unique username.
this.username = username;
}
// Getter for email
public String getEmail() {
return email;
}
// Setter for email with validation
public void setEmail(String email) {
if (email != null && email.contains("@") && email.contains(".")) {
this.email = email;
} else {
System.err.println("Invalid email format for: " + email + ". Email not set.");
// Optionally, throw an exception or set a default/null value
}
}
// Getter for age
public int getAge() {
return age;
}
// Setter for age with validation
public void setAge(int age) {
if (age > 0) {
this.age = age;
} else {
System.err.println("Age must be positive. Age not set or kept as previous.");
}
}
}
// Example 3: Demonstrating encapsulation in the Main method
public class EncapsulationDemo {
public static void main(String[] args) {
// --- BankAccount Demo ---
BankAccount myAccount = new BankAccount("123456789", "John Doe", 1000.0);
// Accessing data through public getters
System.out.println("Account Holder: " + myAccount.getAccountHolder());
System.out.println("Account Number: " + myAccount.getAccountNumber());
System.out.println("Current Balance: $" + myAccount.getBalance());
// Modifying data through public methods (controlled behavior)
myAccount.deposit(500.0);
myAccount.withdraw(200.0);
myAccount.withdraw(2000.0); // Attempt to withdraw too much
myAccount.deposit(-100.0); // Attempt to deposit negative amount
System.out.println("Final Balance: $" + myAccount.getBalance());
// myAccount.balance = 999999.0; // This would cause a compile-time error
// because 'balance' is private. This is encapsulation in action!
System.out.println("\n--- UserProfile Demo ---");
UserProfile user1 = new UserProfile("jsmith", "john.smith@example.com", 25);
System.out.println("Username: " + user1.getUsername());
System.out.println("Email: " + user1.getEmail());
System.out.println("Age: " + user1.getAge());
// Modify attributes using setters with built-in validation
user1.setEmail("invalid-email"); // This will print an error and not change the email.
System.out.println("Email after invalid attempt: " + user1.getEmail());
user1.setAge(-5); // This will print an error and not change the age.
System.out.println("Age after invalid attempt: " + user1.getAge());
user1.setAge(26); // Valid change
System.out.println("Age after valid change: " + user1.getAge());
}
}
Clean Code Tip:
Always hide your class's internal data (attributes) by declaring them private. Expose access to this data only through public Getters (read-only) and Setters (write-only) methods. This practice, known as Encapsulation, allows you to control how the data is accessed and modified, enforce validation rules, and ensures that changes to the internal representation of data don't break external code (a key aspect of the "O" in SOLID - Open/Closed Principle).
Exercise:
Take your Car class. Make its make, model, year, and speed attributes private. Create public Getters for make, model, year, and speed. Also, create a public Setter for speed that only allows setting positive speed and prevents speed from exceeding a MAX_SPEED constant (e.g., 200 km/h). Modify your accelerate and brake methods to use and respect the MAX_SPEED.
Solution:
class Car {
// Private attributes for encapsulation
private String make;
private String model;
private int year;
private int speed; // Current speed, default 0
// Constant for maximum speed
private static final int MAX_SPEED = 200;
// Constructor to initialize the car
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.speed = 0; // Initialize speed to 0
System.out.println("Car " + make + " " + model + " (" + year + ") created.");
}
// Public Getters for attributes
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public int getYear() {
return year;
}
public int getSpeed() {
return speed;
}
// Public Setter for speed with validation
public void setSpeed(int newSpeed) {
if (newSpeed >= 0 && newSpeed <= MAX_SPEED) {
this.speed = newSpeed;
System.out.println(make + " " + model + " speed adjusted to: " + this.speed + " km/h.");
} else if (newSpeed < 0) {
System.err.println("Speed cannot be negative. Speed remains " + this.speed + " km/h.");
} else { // newSpeed > MAX_SPEED
System.err.println("Cannot exceed MAX_SPEED (" + MAX_SPEED + " km/h). Speed remains " + this.speed + " km/h.");
}
}
// Method to accelerate the car, respecting MAX_SPEED
public void accelerate(int amount) {
if (amount > 0) {
int potentialSpeed = this.speed + amount;
if (potentialSpeed <= MAX_SPEED) {
this.speed = potentialSpeed;
System.out.println(make + " " + model + " accelerating. Current speed: " + this.speed + " km/h.");
} else {
this.speed = MAX_SPEED;
System.out.println(make + " " + model + " accelerated to MAX_SPEED. Current speed: " + this.speed + " km/h.");
}
} else {
System.err.println("Acceleration amount must be positive.");
}
}
// Method to brake the car
public void brake(int amount) {
if (amount > 0) {
int potentialSpeed = this.speed - amount;
if (potentialSpeed >= 0) {
this.speed = potentialSpeed;
System.out.println(make + " " + model + " braking. Current speed: " + this.speed + " km/h.");
} else {
this.speed = 0; // Cannot have negative speed
System.out.println(make + " " + model + " has stopped. Current speed: " + this.speed + " km/h.");
}
} else {
System.err.println("Brake amount must be positive.");
}
}
public void startEngine() {
System.out.println(make + " " + model + "'s engine started.");
}
public void displayStatus() {
System.out.println("--- Car Status ---");
System.out.println("Make: " + getMake()); // Using getter
System.out.println("Model: " + getModel()); // Using getter
System.out.println("Year: " + getYear()); // Using getter
System.out.println("Speed: " + getSpeed() + " km/h."); // Using getter
System.out.println("------------------");
}
}
public class CarEncapsulationExerciseSolution {
public static void main(String[] args) {
Car mySportsCar = new Car("Porsche", "911", 2023);
mySportsCar.displayStatus();
mySportsCar.startEngine();
mySportsCar.accelerate(100);
mySportsCar.accelerate(80);
mySportsCar.accelerate(50); // This should hit max speed or cap it
mySportsCar.displayStatus();
// Try to set speed directly (should use setter)
// mySportsCar.speed = 250; // Compile-time error: 'speed' has private access
// Use the setter for controlled modification
mySportsCar.setSpeed(150); // Valid change
mySportsCar.setSpeed(-10); // Invalid change
mySportsCar.setSpeed(300); // Invalid change (exceeds MAX_SPEED)
mySportsCar.brake(mySportsCar.getSpeed()); // Stop the car using its current speed.
mySportsCar.displayStatus();
}
}
Chapter 5: The 'toString()' Method
Quick Theory:
Every class in Java implicitly or explicitly inherits from the Object class. The Object class provides a basic implementation for several methods, one of which is toString(). This method's purpose is to return a string representation of the object. By default, Object's toString() method returns a string consisting of the class name, an '@' sign, and the unsigned hexadecimal representation of the object's hash code (e.g., ClassName@hashCode). While this is useful for unique object identification in memory, it's rarely informative for debugging or logging application-specific object details.
To get a meaningful textual representation of an object's state, it is standard practice to override the toString() method in your own classes. By doing so, you can define exactly what information about your object (typically its attributes) should be included in its string representation. This is incredibly valuable for debugging, logging, and simply understanding an object's current state at any point during program execution.
Professional Code:
// Example 1: Product class WITHOUT overriding toString()
class ProductWithoutToString {
String productId;
String name;
double price;
public ProductWithoutToString(String productId, String name, double price) {
this.productId = productId;
this.name = name;
this.price = price;
}
// No toString() method explicitly defined here.
}
// Example 2: Product class WITH overriding toString()
class ProductWithToString {
String productId;
String name;
double price;
public ProductWithToString(String productId, String name, double price) {
this.productId = productId;
this.name = name;
this.price = price;
}
// Override the toString() method to provide a meaningful string representation.
// This makes debugging and logging much easier.
@Override // This annotation indicates that this method overrides a method in a superclass.
public String toString() {
// We'll return a string containing the object's important attributes.
// String.format is useful for creating formatted strings.
return String.format("Product [ID=%s, Name=%s, Price=%.2f]", productId, name, price);
// Or simply:
// return "Product [ID=" + productId + ", Name=" + name + ", Price=" + price + "]";
}
}
// Example 3: Demonstrating printing objects before and after toString() override
public class ToStringDemo {
public static void main(String[] args) {
// --- Without custom toString() ---
ProductWithoutToString p1_no_string = new ProductWithoutToString("P101", "Laptop", 1200.00);
ProductWithoutToString p2_no_string = new ProductWithoutToString("P102", "Keyboard", 75.50);
System.out.println("--- Products without overridden toString() ---");
// When you print an object directly, Java implicitly calls its toString() method.
// Since it's not overridden, it calls Object's default toString().
System.out.println(p1_no_string);
System.out.println(p2_no_string);
System.out.println(); // For spacing
// --- With custom toString() ---
ProductWithToString p1_with_string = new ProductWithToString("P201", "Smartphone", 999.99);
ProductWithToString p2_with_string = new ProductWithToString("P202", "Headphones", 150.00);
System.out.println("--- Products with overridden toString() ---");
// Now, when you print the object, it calls our custom toString() method,
// providing a much more readable output.
System.out.println(p1_with_string);
System.out.println(p2_with_string);
// This is also useful for logging, error messages, etc.
String logMessage = "Detected issue with product: " + p1_with_string;
System.out.println("\nLog message example: " + logMessage);
}
}
Clean Code Tip:
Always override the toString() method for your domain objects (classes that represent real-world entities or core application concepts). A well-implemented toString() method provides a clear, concise, and helpful textual representation of an object's state, which is invaluable for debugging, logging, and understanding program flow.
Exercise:
Override the toString() method for your Dog class (from previous exercises). The toString() method should return a string like: "Dog [Name: [name], Breed: [breed], Age: [age] years]". In your main method, create a few Dog objects and print them directly to the console to see the effect of your overridden method.
Solution:
class Dog {
private String name;
private String breed;
private int age;
public Dog() {
this("Unnamed", "Mixed", 0); // Chain to parameterized constructor
}
public Dog(String name, String breed, int age) {
this.name = name;
this.breed = breed;
this.age = age;
}
// Getters for attributes (good practice, though not strictly needed for toString)
public String getName() { return name; }
public String getBreed() { return breed; }
public int getAge() { return age; }
public void bark() {
System.out.println(name + " says Woof! Woof!");
}
// Override the toString() method
@Override
public String toString() {
// Return a formatted string representing the Dog object's state
return String.format("Dog [Name: %s, Breed: %s, Age: %d years]", name, breed, age);
}
}
public class DogToStringExerciseSolution {
public static void main(String[] args) {
Dog dog1 = new Dog("Buddy", "Golden Retriever", 5);
Dog dog2 = new Dog("Max", "German Shepherd", 3);
Dog dog3 = new Dog(); // Using the no-arg constructor
System.out.println("--- Printing Dog objects ---");
System.out.println(dog1); // Implicitly calls dog1.toString()
System.out.println(dog2); // Implicitly calls dog2.toString()
System.out.println(dog3); // Implicitly calls dog3.toString()
System.out.println("\n--- Dog actions ---");
dog1.bark();
dog2.bark();
// We can also use toString() explicitly if needed, but it's often implicit.
String dogDetails = dog1.toString();
System.out.println("\nDog 1 details (from explicit call): " + dogDetails);
}
}
Chapter 6: Static vs Instance
Quick Theory: In Java, members of a class (attributes and methods) can be either instance or static. The key difference lies in their ownership and lifecycle. Instance members belong to a specific object (an "instance") of a class. Each object created from a class will have its own copy of instance attributes, and instance methods operate on the state of that particular object. They can only be accessed via an object reference.
Static members, on the other hand, belong to the class itself, not to any specific object. There is only one copy of a static attribute, shared by all instances of the class, and static methods can be called directly on the class name without creating an object. Think of Math.sqrt(): you don't need to create a Math object to use it (new Math().sqrt(16) would be incorrect); you just call Math.sqrt(16). This is because sqrt is a static method, operating independently of any specific Math object's state. Static members are useful for utility functions, constants, or shared data that doesn't vary per object.
Professional Code:
// Example 1: Counter class with instance and static variables
class Counter {
// Instance variable: Each object of Counter will have its own 'instanceCount'.
// It keeps track of a count specific to that instance.
private int instanceCount;
// Static variable: Belongs to the class itself, not to any specific object.
// There's only one 'totalInstances' across all Counter objects.
private static int totalInstances = 0; // Initialized once when the class is loaded.
public Counter() {
// When a new Counter object is created, we increment both counts.
this.instanceCount = 0; // Initialize instance-specific count
totalInstances++; // Increment the class-wide total count
System.out.println("New Counter object created. Total instances: " + totalInstances);
}
// Instance method: Operates on the 'instanceCount' of THIS specific object.
public void incrementInstanceCount() {
this.instanceCount++;
System.out.println("Instance count for this object: " + this.instanceCount);
}
// Static method: Operates on 'totalInstances' (the class-level data).
// It can be called without creating an object.
public static int getTotalInstances() {
// A static method cannot directly access instance variables (like 'instanceCount')
// because it doesn't belong to a specific object.
return totalInstances;
}
public int getInstanceCount() {
return instanceCount;
}
}
// Example 2: Utility class with static methods
// Static methods are often used for utility classes that don't need object state.
class CalculatorUtils {
// Static constant: A value that belongs to the class and doesn't change.
// 'final' means its value cannot be reassigned after initialization.
public static final double PI = 3.1415926535;
// Static method to calculate the area of a circle.
// It doesn't need any object-specific data, just parameters.
public static double calculateCircleArea(double radius) {
if (radius < 0) {
throw new IllegalArgumentException("Radius cannot be negative.");
}
return PI * radius * radius;
}
// Static method to sum two numbers.
public static int add(int a, int b) {
return a + b;
}
// Static method to format a string, demonstrating a common utility pattern.
public static String formatGreeting(String name) {
return "Hello, " + name + "!";
}
}
// Example 3: Demonstrating static vs. instance behavior
public class StaticVsInstanceDemo {
public static void main(String[] args) {
// --- Counter Demo ---
System.out.println("--- Counter Demo ---");
// Accessing static method directly via the class name.
System.out.println("Initial total instances: " + Counter.getTotalInstances()); // 0
Counter c1 = new Counter(); // Constructor increments totalInstances to 1
c1.incrementInstanceCount(); // Increments c1's instanceCount
c1.incrementInstanceCount(); // Increments c1's instanceCount again
System.out.println("c1's instance count: " + c1.getInstanceCount());
System.out.println("Current total instances: " + Counter.getTotalInstances()); // 1
Counter c2 = new Counter(); // Constructor increments totalInstances to 2
c2.incrementInstanceCount(); // Increments c2's instanceCount
System.out.println("c2's instance count: " + c2.getInstanceCount());
System.out.println("Current total instances: " + Counter.getTotalInstances()); // 2
// Notice that c1.instanceCount (2) and c2.instanceCount (1) are independent,
// but Counter.totalInstances (2) is shared.
// --- CalculatorUtils Demo ---
System.out.println("\n--- CalculatorUtils Demo ---");
// Accessing static constant directly via the class name.
System.out.println("Value of PI: " + CalculatorUtils.PI);
// Calling static methods directly via the class name.
double area = CalculatorUtils.calculateCircleArea(5.0);
System.out.println("Area of circle with radius 5: " + area);
int sum = CalculatorUtils.add(10, 20);
System.out.println("Sum of 10 and 20: " + sum);
String greeting = CalculatorUtils.formatGreeting("Alice");
System.out.println(greeting);
// We don't need to create an object of CalculatorUtils to use its methods.
// CalculatorUtils myUtil = new CalculatorUtils(); // This is often unnecessary for utility classes.
}
}
Clean Code Tip:
Use static for methods that do not depend on the state of an object and for constants that are shared across all instances of a class or relate globally to the class itself. Avoid using static for mutable state that should be unique to each object, as this can lead to hard-to-track bugs due to shared mutable state.
Exercise:
Create a Circle class. It should have an instance attribute radius (double). It should also have a static constant PI (double, value 3.14159). Add an instance method getArea() that calculates the area based on the circle's radius. Then, add a static method getCircumference(double radius) that calculates circumference for any given radius, without needing a Circle object. In your main method, demonstrate creating Circle objects and calling both instance and static methods.
Solution:
class Circle {
// Static constant: Belongs to the class, shared by all instances.
public static final double PI = 3.14159;
// Instance attribute: Belongs to each individual Circle object.
private double radius;
// Constructor to initialize the radius of a specific Circle object.
public Circle(double radius) {
if (radius < 0) {
throw new IllegalArgumentException("Radius cannot be negative.");
}
this.radius = radius;
System.out.println("Circle created with radius: " + this.radius);
}
// Instance method: Calculates area for *this specific* Circle object.
// It uses the 'radius' attribute of the current instance.
public double getArea() {
return PI * this.radius * this.radius;
}
// Getter for radius (instance method)
public double getRadius() {
return radius;
}
// Setter for radius (instance method)
public void setRadius(double radius) {
if (radius < 0) {
throw new IllegalArgumentException("Radius cannot be negative.");
}
this.radius = radius;
System.out.println("Radius updated to: " + this.radius);
}
// Static method: Calculates circumference for *any* given radius.
// It doesn't rely on the 'radius' attribute of a specific Circle object.
// It can be called directly on the class.
public static double getCircumference(double anyRadius) {
if (anyRadius < 0) {
throw new IllegalArgumentException("Radius cannot be negative.");
}
return 2 * PI * anyRadius;
}
@Override
public String toString() {
return String.format("Circle [Radius: %.2f, Area: %.2f]", radius, getArea());
}
}
public class CircleStaticVsInstanceExerciseSolution {
public static void main(String[] args) {
// --- Accessing Static Members ---
System.out.println("Value of PI from Circle class: " + Circle.PI);
// Calling a static method directly on the class name.
double circumferenceOfRadius10 = Circle.getCircumference(10.0);
System.out.println("Circumference for radius 10.0: " + circumferenceOfRadius10);
System.out.println("\n--- Creating and using Circle Objects (Instance Members) ---");
// Create a Circle object (instance)
Circle circle1 = new Circle(7.5);
// Access instance method 'getArea()' for circle1
System.out.println("Area of circle1: " + circle1.getArea());
System.out.println("Radius of circle1: " + circle1.getRadius());
System.out.println(circle1);
// Create another Circle object (another instance)
Circle circle2 = new Circle(3.0);
System.out.println("Area of circle2: " + circle2.getArea());
System.out.println(circle2);
// You can use the static method with an object's radius, but it's called on the class.
System.out.println("Circumference of circle1 (using static method): " + Circle.getCircumference(circle1.getRadius()));
// Modify circle1's radius and see the area change (instance behavior)
circle1.setRadius(10.0);
System.out.println(circle1);
}
}
Chapter 7: Inheritance (Extends)
Quick Theory: Inheritance is a fundamental concept in Object-Oriented Programming that promotes reusability and specialization. It allows a new class (the subclass or child class) to inherit properties (attributes) and behaviors (methods) from an existing class (the superclass or parent class). This means the subclass automatically gets all the non-private members of its superclass, saving development time and ensuring a consistent base structure.
The power of inheritance also lies in its flexibility to model "is-a" relationships. For example, a Dog is a Animal, and a Car is a Vehicle. The subclass can then extend or specialize the inherited features, adding its unique attributes and methods, or even modifying the behavior of inherited methods (which we'll cover in the next chapter). The super() keyword is crucial in a subclass's constructor to explicitly call a constructor of its parent class, ensuring that the parent part of the object is properly initialized before the child's specific initialization.
Professional Code:
// Example 1: Animal (Superclass) and Dog (Subclass) demonstrating basic inheritance
// Superclass: Animal
class Animal {
String name;
int age;
// Constructor for the Animal class
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Animal constructor called for " + name);
}
// Method common to all animals
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
// Subclass: Dog, inherits from Animal using the 'extends' keyword
class Dog extends Animal {
String breed;
// Constructor for Dog
// The 'super(name, age)' call invokes the constructor of the parent class (Animal).
// This must be the very first statement in the subclass constructor.
public Dog(String name, int age, String breed) {
super(name, age); // Initialize inherited 'name' and 'age' from Animal
this.breed = breed; // Initialize Dog-specific 'breed'
System.out.println("Dog constructor called for " + name + " (Breed: " + breed + ")");
}
// Dog-specific method
public void bark() {
System.out.println(name + " barks: Woof! Woof!");
}
// Dog can also use inherited methods like eat() and sleep()
public void displayDogInfo() {
System.out.println("--- Dog Info ---");
System.out.println("Name: " + name); // 'name' is inherited from Animal
System.out.println("Age: " + age); // 'age' is inherited from Animal
System.out.println("Breed: " + breed); // 'breed' is specific to Dog
}
}
// Example 2: Demonstrating chained inheritance (Vehicle -> Car -> ElectricCar)
// Grandparent class: Vehicle
class Vehicle {
String manufacturer;
int year;
public Vehicle(String manufacturer, int year) {
this.manufacturer = manufacturer;
this.year = year;
System.out.println("Vehicle constructor called for " + manufacturer);
}
public void start() {
System.out.println("Vehicle started.");
}
}
// Parent class: Car, inherits from Vehicle
class Car extends Vehicle {
String model;
int numberOfDoors;
public Car(String manufacturer, int year, String model, int numberOfDoors) {
super(manufacturer, year); // Call Vehicle's constructor
this.model = model;
this.numberOfDoors = numberOfDoors;
System.out.println("Car constructor called for " + model);
}
public void drive() {
System.out.println("The " + manufacturer + " " + model + " is driving.");
}
}
// Child class: ElectricCar, inherits from Car (and indirectly from Vehicle)
class ElectricCar extends Car {
int batteryCapacityKWh;
public ElectricCar(String manufacturer, int year, String model, int numberOfDoors, int batteryCapacityKWh) {
super(manufacturer, year, model, numberOfDoors); // Call Car's constructor
this.batteryCapacityKWh = batteryCapacityKWh;
System.out.println("ElectricCar constructor called with " + batteryCapacityKWh + " kWh battery.");
}
public void charge() {
System.out.println("The " + model + " is charging its " + batteryCapacityKWh + " kWh battery.");
}
public void displayElectricCarInfo() {
System.out.println("--- Electric Car Info ---");
System.out.println("Manufacturer: " + manufacturer); // Inherited from Vehicle
System.out.println("Year: " + year); // Inherited from Vehicle
System.out.println("Model: " + model); // Inherited from Car
System.out.println("Doors: " + numberOfDoors); // Inherited from Car
System.out.println("Battery: " + batteryCapacityKWh + " kWh"); // Specific to ElectricCar
}
}
// Main method to demonstrate inheritance
public class InheritanceDemo {
public static void main(String[] args) {
// Demonstrate Animal and Dog
Animal generalAnimal = new Animal("Babe", 2);
generalAnimal.eat();
generalAnimal.sleep();
System.out.println("\n--- Dog Object ---");
Dog myDog = new Dog("Buddy", 5, "Golden Retriever");
myDog.displayDogInfo();
myDog.eat(); // Inherited from Animal
myDog.bark(); // Specific to Dog
myDog.sleep(); // Inherited from Animal
System.out.println("\n--- ElectricCar Object (Chained Inheritance) ---");
ElectricCar tesla = new ElectricCar("Tesla", 2023, "Model 3", 4, 75);
tesla.displayElectricCarInfo();
tesla.start(); // Inherited from Vehicle
tesla.drive(); // Inherited from Car
tesla.charge(); // Specific to ElectricCar
}
}
Clean Code Tip:
Use inheritance only when there's a clear "is-a" relationship (e.g., a Dog is an Animal). If a class "has-a" relationship (e.g., a Car has an Engine), prefer composition (where one class contains an object of another class) over inheritance. Overuse of inheritance can lead to rigid hierarchies.
Exercise:
Create an Employee class with attributes name (String), employeeId (String), and salary (double). It should have a constructor and a displayInfo() method. Then, create a Manager class that extends Employee. Manager should add an attribute department (String) and override the displayInfo() method to include the department information. In your main method, create an Employee and a Manager object and call displayInfo() for both.
Solution:
// Superclass: Employee
class Employee {
String name;
String employeeId;
double salary;
public Employee(String name, String employeeId, double salary) {
this.name = name;
this.employeeId = employeeId;
this.salary = salary;
System.out.println("Employee created: " + name);
}
public void displayInfo() {
System.out.println("--- Employee Info ---");
System.out.println("Name: " + name);
System.out.println("Employee ID: " + employeeId);
System.out.println("Salary: $" + String.format("%.2f", salary));
}
}
// Subclass: Manager, inherits from Employee
class Manager extends Employee {
String department;
public Manager(String name, String employeeId, double salary, String department) {
super(name, employeeId, salary); // Call Employee's constructor
this.department = department;
System.out.println("Manager created: " + name + " (Dept: " + department + ")");
}
// Override displayInfo() to add department information
// We'll dive into @Override more in the next chapter!
@Override
public void displayInfo() {
super.displayInfo(); // Call the parent's displayInfo() first
System.out.println("Department: " + department);
System.out.println("---------------------");
}
}
public class EmployeeHierarchySolution {
public static void main(String[] args) {
Employee emp1 = new Employee("Alice Wonderland", "EMP001", 60000.00);
emp1.displayInfo();
System.out.println(); // Spacing
Manager mgr1 = new Manager("Bob The Builder", "MGR001", 90000.00, "Construction");
mgr1.displayInfo();
System.out.println(); // Spacing
Employee emp2 = new Employee("Charlie Chaplin", "EMP002", 55000.00);
emp2.displayInfo();
System.out.println(); // Spacing
Manager mgr2 = new Manager("Diana Prince", "MGR002", 95000.00, "Marketing");
mgr2.displayInfo();
}
}
Chapter 8: Method Overriding (@Override)
Quick Theory: Method Overriding is a key aspect of polymorphism (which we'll cover later) and allows for customizing inherited behavior, enhancing flexibility. When a subclass provides a specific implementation for a method that is already defined in its superclass, it is said to be overriding that method. The method signature (name, return type, and parameters) must be exactly the same as in the superclass. This ensures reusability of the method name, but with specialized logic for the child class.
The @Override annotation is highly recommended when overriding a method. It's a compiler instruction that tells Java you intend to override a superclass method. If you make a mistake in the signature, the compiler will alert you, preventing subtle bugs. It's important to distinguish overriding from overloading: Overloading involves methods with the same name but different parameter lists within the same class (or across hierarchy), while overriding involves methods with identical signatures in a subclass to provide a specialized implementation.
Professional Code:
// Example 1: Animal (superclass) with makeSound() overridden by Dog and Cat
// Superclass: Animal
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
// This method will be overridden by subclasses.
public void makeSound() {
System.out.println(name + " makes a generic animal sound.");
}
}
// Subclass: Dog, overrides makeSound()
class Dog extends Animal {
public Dog(String name) {
super(name);
}
// @Override annotation is good practice. It tells the compiler
// that this method is intended to override a method in the superclass.
// If the signature doesn't match, the compiler will throw an error.
@Override
public void makeSound() {
System.out.println(name + " barks: Woof! Woof!");
}
// Dog-specific method
public void fetch() {
System.out.println(name + " fetches the ball!");
}
}
// Subclass: Cat, also overrides makeSound()
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " meows: Meow!");
}
// Cat-specific method
public void scratch() {
System.out.println(name + " scratches the furniture!");
}
}
// Example 2: Vehicle with start() method, overridden by Car, and using super.method()
// Superclass: Vehicle
class Vehicle {
String type;
public Vehicle(String type) {
this.type = type;
}
public void start() {
System.out.println("The " + type + " is starting its engine.");
}
// Example of an overloaded method (different parameters) - NOT overriding
public void start(String keyType) {
System.out.println("The " + type + " is starting with a " + keyType + " key.");
}
public void stop() {
System.out.println("The " + type + " has stopped.");
}
}
// Subclass: Car, overrides start() and uses super.start()
class Car extends Vehicle {
String model;
public Car(String model) {
super("Car"); // Type for Vehicle
this.model = model;
}
@Override
public void start() {
// Calling the superclass's (Vehicle's) start() method first.
// This allows us to reuse the parent's logic and then add specific logic.
super.start();
System.out.println("The " + model + " specific startup routine is engaged.");
System.out.println("Checking fuel levels for " + model + "...");
}
// This is overloading, not overriding. Same method name, different parameters.
public void start(boolean pushButton) {
if (pushButton) {
System.out.println("The " + model + " is starting with a push button.");
} else {
// Can call the overloaded parent method as well if appropriate
super.start("mechanical");
}
}
public void drive() {
System.out.println("The " + model + " is driving down the road.");
}
}
// Main method to demonstrate method overriding and overloading
public class MethodOverridingDemo {
public static void main(String[] args) {
// --- Animal Sounds Demo ---
Animal animal = new Animal("Generic");
animal.makeSound(); // Calls Animal's makeSound()
Dog dog = new Dog("Buddy");
dog.makeSound(); // Calls Dog's overridden makeSound()
dog.fetch();
Cat cat = new Cat("Whiskers");
cat.makeSound(); // Calls Cat's overridden makeSound()
cat.scratch();
System.out.println("\n--- Vehicle/Car Startup Demo ---");
Vehicle truck = new Vehicle("Truck");
truck.start(); // Calls Vehicle's start()
truck.start("electronic"); // Calls Vehicle's overloaded start()
Car sedan = new Car("Honda Civic");
sedan.start(); // Calls Car's overridden start(), which also calls super.start()
sedan.start(true); // Calls Car's overloaded start() with boolean parameter
sedan.drive();
sedan.stop();
}
}
Clean Code Tip:
Always use the @Override annotation when you intend to override a method. This is not just documentation; it's a critical safety mechanism that allows the compiler to catch errors if your method signature doesn't actually match a superclass method, saving you from subtle bugs at runtime.
Exercise:
Create a Shape class with a method draw() that prints "Drawing a generic shape." Create two subclasses, Circle and Rectangle, both extending Shape. Override the draw() method in each subclass to print "Drawing a Circle." and "Drawing a Rectangle." respectively. In Rectangle, also add an area() method. Demonstrate calling draw() for all three types of objects.
Solution:
// Superclass: Shape
class Shape {
public void draw() {
System.out.println("Drawing a generic shape.");
}
}
// Subclass: Circle
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle.");
}
}
// Subclass: Rectangle
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a Rectangle.");
}
public double calculateArea() {
return width * height;
}
}
public class ShapeDrawingSolution {
public static void main(String[] args) {
Shape genericShape = new Shape();
Circle myCircle = new Circle();
Rectangle myRectangle = new Rectangle(10, 5);
System.out.println("--- Drawing various shapes ---");
genericShape.draw(); // Calls Shape's draw()
myCircle.draw(); // Calls Circle's overridden draw()
myRectangle.draw(); // Calls Rectangle's overridden draw()
System.out.println("\n--- Rectangle Specific ---");
System.out.println("Rectangle Area: " + myRectangle.calculateArea());
}
}
Chapter 9: Abstract Classes & Methods
Quick Theory:
Abstract classes are special classes that cannot be instantiated directly; they serve as blueprints for other classes, embodying the "is-a" relationship in a more forceful way. They are designed to be inherited from, providing a common base structure and some default implementations for subclasses. What makes them unique is the ability to declare abstract methods. An abstract method has a signature but no body; it must be implemented by any concrete (non-abstract) subclass. This mechanism enforces a contract, ensuring that all concrete subclasses provide a specific behavior, promoting reusability of the general structure while guaranteeing flexibility in implementation details.
An abstract class can have both abstract and concrete (regular) methods, as well as instance variables and constructors. Its primary purpose is to define a common interface and possibly some shared functionality for a group of related classes, while deferring certain specific implementations to its descendants. This is ideal when you want to define a general type but know that some of its behaviors are too specialized to implement in a generic way, leaving them to the subclasses to define.
Professional Code:
// Example 1: Abstract Shape class with abstract calculateArea() and concrete displayInfo()
// Abstract class: Cannot be instantiated directly.
// It serves as a base for specific shapes.
abstract class Shape {
String color;
public Shape(String color) {
this.color = color;
}
// Abstract method: Has no body and must be implemented by concrete subclasses.
// This forces all shapes to define how they calculate their area.
public abstract double calculateArea();
// Concrete method: Has an implementation and can be inherited directly.
public void displayInfo() {
System.out.println("This is a " + color + " shape.");
}
}
// Concrete subclass: Circle, extends Shape and implements calculateArea()
class Circle extends Shape {
double radius;
public Circle(String color, double radius) {
super(color); // Call Shape's constructor
this.radius = radius;
}
// Must implement the abstract method from Shape.
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
// Can add Circle-specific methods
public void roll() {
System.out.println("The " + color + " circle is rolling.");
}
}
// Concrete subclass: Rectangle, extends Shape and implements calculateArea()
class Rectangle extends Shape {
double width;
double height;
public Rectangle(String color, double width, double height) {
super(color); // Call Shape's constructor
this.width = width;
this.height = height;
}
// Must implement the abstract method from Shape.
@Override
public double calculateArea() {
return width * height;
}
}
// Example 2: Abstract Employee class with abstract calculateSalary()
// Abstract class: Employee
abstract class Employee {
String name;
String employeeId;
public Employee(String name, String employeeId) {
this.name = name;
this.employeeId = employeeId;
}
// Abstract method: Salary calculation varies greatly depending on employee type.
// Subclasses *must* define how their salary is calculated.
public abstract double calculateSalary();
// Concrete method: General information for all employees.
public void displayBasicInfo() {
System.out.println("Employee Name: " + name + ", ID: " + employeeId);
}
}
// Concrete subclass: FullTimeEmployee
class FullTimeEmployee extends Employee {
double monthlySalary;
public FullTimeEmployee(String name, String employeeId, double monthlySalary) {
super(name, employeeId);
this.monthlySalary = monthlySalary;
}
@Override
public double calculateSalary() {
return monthlySalary; // Full-time employees have a fixed monthly salary.
}
public void processBenefits() {
System.out.println(name + " is eligible for full-time benefits.");
}
}
// Concrete subclass: PartTimeEmployee
class PartTimeEmployee extends Employee {
double hourlyRate;
int hoursWorked;
public PartTimeEmployee(String name, String employeeId, double hourlyRate, int hoursWorked) {
super(name, employeeId);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
@Override
public double calculateSalary() {
return hourlyRate * hoursWorked; // Part-time employees get paid by the hour.
}
public void trackHours() {
System.out.println(name + " has worked " + hoursWorked + " hours this period.");
}
}
// Main method to demonstrate abstract classes and methods
public class AbstractClassDemo {
public static void main(String[] args) {
// --- Shape Demo ---
// Shape s = new Shape("green"); // Compile-time error: Cannot instantiate abstract class
Circle redCircle = new Circle("Red", 5.0);
redCircle.displayInfo();
System.out.println("Circle Area: " + redCircle.calculateArea());
redCircle.roll();
System.out.println(); // Spacing
Rectangle blueRectangle = new Rectangle("Blue", 10.0, 4.0);
blueRectangle.displayInfo();
System.out.println("Rectangle Area: " + blueRectangle.calculateArea());
System.out.println("\n--- Employee Demo ---");
FullTimeEmployee ftEmployee = new FullTimeEmployee("John Doe", "FT101", 5000.0);
ftEmployee.displayBasicInfo();
System.out.println("Full-time Salary: $" + String.format("%.2f", ftEmployee.calculateSalary()));
ftEmployee.processBenefits();
System.out.println(); // Spacing
PartTimeEmployee ptEmployee = new PartTimeEmployee("Jane Smith", "PT202", 25.0, 160);
ptEmployee.displayBasicInfo();
System.out.println("Part-time Salary: $" + String.format("%.2f", ptEmployee.calculateSalary()));
ptEmployee.trackHours();
}
}
Clean Code Tip: Use abstract classes when you have a strong "is-a" relationship, and you want to provide a common base with some implemented (concrete) methods, but also enforce that all concrete subclasses implement certain specific behaviors (abstract methods). This creates a clear hierarchy and ensures adherence to a design contract within that hierarchy.
Exercise:
Create an abstract class named Vehicle. It should have a private String brand and a constructor. It should have an abstract method startEngine() and a concrete method stopEngine() that prints "Engine stopped.". Then, create two concrete subclasses: Car and Motorcycle. Both must extend Vehicle and implement startEngine() to print specific startup messages (e.g., "Car engine starting...", "Motorcycle engine starting..."). In your main method, instantiate a Car and a Motorcycle and call their methods.
Solution:
// Abstract class: Vehicle
abstract class Vehicle {
private String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public String getBrand() { // Getter for private attribute
return brand;
}
// Abstract method: Must be implemented by subclasses
public abstract void startEngine();
// Concrete method: Implemented here, inherited by subclasses
public void stopEngine() {
System.out.println(brand + " engine stopped.");
}
}
// Concrete subclass: Car
class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
@Override
public void startEngine() {
System.out.println(getBrand() + " car engine starting with a turn of the key.");
}
public void honk() {
System.out.println(getBrand() + " car honks: Beep beep!");
}
}
// Concrete subclass: Motorcycle
class Motorcycle extends Vehicle {
public Motorcycle(String brand) {
super(brand);
}
@Override
public void startEngine() {
System.out.println(getBrand() + " motorcycle engine roaring to life.");
}
public void wheelie() {
System.out.println(getBrand() + " motorcycle is doing a wheelie!");
}
}
public class VehicleAbstractExerciseSolution {
public static void main(String[] args) {
// Vehicle genericVehicle = new Vehicle("Generic"); // Compile-time error
Car myCar = new Car("Toyota");
myCar.startEngine();
myCar.honk();
myCar.stopEngine();
System.out.println(); // Spacing
Motorcycle myMotorcycle = new Motorcycle("Harley-Davidson");
myMotorcycle.startEngine();
myMotorcycle.wheelie();
myMotorcycle.stopEngine();
}
}
Chapter 10: Interfaces (Implements)
Quick Theory:
Interfaces in Java define a contract: a set of abstract methods that a class must implement if it wants to adhere to that interface. Unlike abstract classes, which model "is-a" relationships in a hierarchy, interfaces model "can-do" capabilities or behaviors. A class can implement multiple interfaces, allowing it to take on various "roles" or capabilities without being forced into a single inheritance hierarchy. This dramatically increases flexibility as it decouples the definition of behavior from its implementation, enabling different, unrelated classes to share common functionalities.
Interfaces are purely abstract by default (all fields are public static final and all methods are public abstract before Java 8). Since Java 8, interfaces can also include default methods, which provide a default implementation that implementing classes can use directly or override. This addition allows interfaces to evolve without breaking existing code and provides some reusability of method implementations within the contract itself. Interfaces cannot have constructors or instance variables.
Professional Code:
// Example 1: Flyable interface implemented by Bird and Airplane
// Interface: Defines a contract for anything that can fly.
// All methods in an interface are implicitly public and abstract before Java 8.
interface Flyable {
void fly(); // Abstract method: no body.
void land(); // Abstract method: no body.
// Default method (Java 8+): Provides a default implementation.
// Classes can use this directly or override it.
default void reportFlightStatus() {
System.out.println("Currently in flight. Altitude unknown.");
}
}
// Class: Bird, implements the Flyable interface
class Bird implements Flyable {
String name;
public Bird(String name) {
this.name = name;
}
// Must implement all abstract methods from Flyable.
@Override
public void fly() {
System.out.println(name + " is flapping its wings and flying.");
}
@Override
public void land() {
System.out.println(name + " is gracefully landing on a branch.");
}
// Can optionally override default methods
@Override
public void reportFlightStatus() {
System.out.println(name + " is flying high, probably looking for worms.");
}
}
// Class: Airplane, implements the Flyable interface
class Airplane implements Flyable {
String model;
public Airplane(String model) {
this.model = model;
}
// Must implement all abstract methods from Flyable.
@Override
public void fly() {
System.out.println(model + " is soaring through the sky with jet engines.");
}
@Override
public void land() {
System.out.println(model + " is making a smooth landing on the runway.");
}
// Using the default method without overriding.
// reportFlightStatus() will print "Currently in flight. Altitude unknown."
}
// Example 2: Multiple interfaces (Drivable, Maintainable) implemented by Car
// Interface 1: Drivable (defines driving capabilities)
interface Drivable {
void accelerate();
void brake();
// Default method for Drivable
default void horn() {
System.out.println("Default horn sound.");
}
}
// Interface 2: Maintainable (defines maintenance capabilities)
interface Maintainable {
void scheduleMaintenance();
void performService();
}
// Class: SportsCar, implements both Drivable and Maintainable interfaces
class SportsCar implements Drivable, Maintainable {
String brand;
String model;
public SportsCar(String brand, String model) {
this.brand = brand;
this.model = model;
}
// Implementing Drivable methods
@Override
public void accelerate() {
System.out.println(brand + " " + model + " is accelerating with immense power!");
}
@Override
public void brake() {
System.out.println(brand + " " + model + " is braking hard.");
}
// Overriding the default horn method from Drivable
@Override
public void horn() {
System.out.println(brand + " " + model + " blasts a loud, sporty horn!");
}
// Implementing Maintainable methods
@Override
public void scheduleMaintenance() {
System.out.println("Scheduling performance maintenance for " + brand + " " + model + ".");
}
@Override
public void performService() {
System.out.println("Performing engine tune-up and oil change for " + brand + " " + model + ".");
}
// SportsCar-specific method
public void activateNitro() {
System.out.println(brand + " " + model + " activates nitro boost!");
}
}
// Main method to demonstrate interfaces and default methods
public class InterfaceDemo {
public static void main(String[] args) {
// --- Flyable Demo ---
System.out.println("--- Bird Flight ---");
Bird eagle = new Bird("Eagle");
eagle.fly();
eagle.reportFlightStatus(); // Calls overridden default method
eagle.land();
System.out.println("\n--- Airplane Flight ---");
Airplane boeing = new Airplane("Boeing 747");
boeing.fly();
boeing.reportFlightStatus(); // Calls the default method from interface
boeing.land();
// --- Multiple Interfaces Demo ---
System.out.println("\n--- SportsCar Capabilities ---");
SportsCar ferrari = new SportsCar("Ferrari", "488 GTB");
ferrari.accelerate();
ferrari.horn(); // Calls overridden horn method
ferrari.brake();
ferrari.activateNitro();
ferrari.scheduleMaintenance();
ferrari.performService();
}
}
Clean Code Tip: Use interfaces to define capabilities or contracts that different, often unrelated, classes can fulfill. This promotes loose coupling and allows for highly flexible designs where classes can interact based on their capabilities rather than their concrete types. "Program to an interface, not an implementation" is a core tenet of good OOP design.
Exercise:
Create an interface Edible with a method howToEat() that returns a String. Create two classes: Apple and Chicken. Both should implement Edible. Apple's howToEat() should return "Bite into it." and Chicken's howToEat() should return "Cook and then eat.". In your main method, create objects of both classes and call their howToEat() method.
Solution:
// Interface: Edible
interface Edible {
String howToEat();
}
// Class: Apple, implements Edible
class Apple implements Edible {
@Override
public String howToEat() {
return "Bite into it.";
}
public String getType() {
return "Fruit";
}
}
// Class: Chicken, implements Edible
class Chicken implements Edible {
@Override
public String howToEat() {
return "Cook and then eat.";
}
public String getPart() {
return "Breast";
}
}
public class EdibleInterfaceExerciseSolution {
public static void main(String[] args) {
Apple anApple = new Apple();
Chicken aChicken = new Chicken();
System.out.println("--- How to eat different things ---");
System.out.println("Apple: " + anApple.howToEat());
System.out.println("Chicken: " + aChicken.howToEat());
System.out.println("\n--- Object specific methods ---");
System.out.println("Apple type: " + anApple.getType());
System.out.println("Chicken part: " + aChicken.getPart());
}
}
Chapter 11: Polymorphism
Quick Theory: Polymorphism, meaning "many forms," is a core concept in OOP that allows objects of different classes to be treated as objects of a common type (either a superclass or an interface). This concept significantly enhances reusability and flexibility by enabling you to write generic code that can operate on a variety of objects, without needing to know their exact concrete type at compile time. The actual method executed is determined at runtime based on the object's real type, a principle known as "dynamic method dispatch" or "runtime polymorphism."
Polymorphism works hand-in-hand with inheritance and interfaces. If a Dog is a Animal, then a Dog object can be referred to by an Animal reference. Similarly, if a Car implements Drivable, a Car object can be referred to by a Drivable reference. This means you can create collections of different types of animals (ArrayList<Animal>) or different types of drivable objects (List<Drivable>) and interact with them using the common methods defined in the superclass or interface, letting each object respond in its own specific way.
Professional Code:
// Example 1: Polymorphism with an Animal hierarchy
// Base class for polymorphism
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void makeSound() {
System.out.println(name + " makes a generic animal sound.");
}
}
// Subclass Dog
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " barks: Woof! Woof!");
}
public void fetch() {
System.out.println(getName() + " fetches the stick.");
}
}
// Subclass Cat
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " meows: Meow!");
}
public void purr() {
System.out.println(getName() + " purrs softly.");
}
}
// Subclass Bird
class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " chirps: Tweet! Tweet!");
}
public void fly() {
System.out.println(getName() + " is flying.");
}
}
// Example 2: Polymorphism with an interface (Payable)
// Interface: Defines a contract for anything that can be paid.
interface Payable {
double getPaymentAmount();
void processPayment();
}
// Class: Invoice, implements Payable
class Invoice implements Payable {
private String partNumber;
private int quantity;
private double pricePerItem;
public Invoice(String partNumber, int quantity, double pricePerItem) {
this.partNumber = partNumber;
this.quantity = quantity;
this.pricePerItem = pricePerItem;
}
@Override
public double getPaymentAmount() {
return quantity * pricePerItem;
}
@Override
public void processPayment() {
System.out.println("Processing payment for Invoice #" + partNumber + ". Amount: $" + String.format("%.2f", getPaymentAmount()));
}
public void sendInvoiceEmail() {
System.out.println("Invoice email sent for " + partNumber);
}
}
// Class: Employee (reusing from Chapter 7/9 concepts, now implements Payable)
class Employee implements Payable {
private String name;
private String employeeId;
private double monthlySalary; // Simplified for payment
public Employee(String name, String employeeId, double monthlySalary) {
this.name = name;
this.employeeId = employeeId;
this.monthlySalary = monthlySalary;
}
@Override
public double getPaymentAmount() {
return monthlySalary;
}
@Override
public void processPayment() {
System.out.println("Processing monthly salary for " + name + " (ID: " + employeeId + "). Amount: $" + String.format("%.2f", getPaymentAmount()));
}
public void generatePayStub() {
System.out.println("Pay stub generated for " + name);
}
}
// Main method to demonstrate polymorphism
import java.util.ArrayList;
import java.util.List;
public class PolymorphismDemo {
public static void main(String[] args) {
// --- Animal Polymorphism Demo ---
System.out.println("--- Animal Polymorphism ---");
// Create a list that can hold any object of type Animal or its subclasses.
List<Animal> animals = new ArrayList<>();
animals.add(new Dog("Buddy"));
animals.add(new Cat("Whiskers"));
animals.add(new Bird("Tweety"));
animals.add(new Animal("Unknown Creature")); // Can add the base type too
// Iterate through the list. Even though we add different types of animals,
// we can treat them all as 'Animal' objects.
// The 'makeSound()' method called will be the specific overridden version
// for each object at runtime (dynamic method dispatch).
for (Animal animal : animals) {
animal.makeSound(); // This is polymorphism in action!
}
// This demonstrates flexibility: we can add more Animal types later
// without changing this loop.
// --- Payable Interface Polymorphism Demo ---
System.out.println("\n--- Payable Interface Polymorphism ---");
// Create a list that can hold any object that implements the Payable interface.
List<Payable> payables = new ArrayList<>();
payables.add(new Invoice("PN-9876", 2, 125.00));
payables.add(new Employee("Alice Smith", "E001", 3500.00));
payables.add(new Invoice("PN-5432", 1, 750.50));
// Process payments for all payable items.
// The 'processPayment()' method will behave differently for Invoice and Employee,
// even though we call it through the common 'Payable' interface.
for (Payable payableItem : payables) {
payableItem.processPayment();
}
System.out.println("\n--- Mixed Polymorphism ---");
// You can even assign a subclass object to a superclass reference variable
Animal polyDog = new Dog("Rex");
polyDog.makeSound(); // Calls Dog's makeSound()
// polyDog.fetch(); // Compile-time error: 'fetch' is not defined in Animal (compile-time type)
Payable polyEmployee = new Employee("Bob Johnson", "E002", 4000.00);
polyEmployee.processPayment(); // Calls Employee's processPayment()
// polyEmployee.generatePayStub(); // Compile-time error: 'generatePayStub' not defined in Payable
}
}
Clean Code Tip: Always "Program to an interface, not an implementation." This means that wherever possible, declare variables, method parameters, and return types using the most general type possible (an interface or a superclass), rather than a specific concrete class. This makes your code more flexible, reusable, and easier to maintain or extend in the future.
Exercise:
Create an abstract class MediaItem with title (String) and duration (int minutes). It should have a constructor and an abstract method displayDetails(). Create two subclasses: Movie and Book (yes, a book has a "duration" in reading time). Movie should add a director (String) and override displayDetails(). Book should add author (String) and override displayDetails(). In your main method, create an ArrayList<MediaItem>, add a Movie and a Book object, then iterate through the list and call displayDetails() for each.
Solution:
import java.util.ArrayList;
import java.util.List;
// Abstract class: MediaItem
abstract class MediaItem {
protected String title;
protected int durationMinutes; // Could be movie length or reading time
public MediaItem(String title, int durationMinutes) {
this.title = title;
this.durationMinutes = durationMinutes;
}
public String getTitle() {
return title;
}
public abstract void displayDetails();
}
// Subclass: Movie
class Movie extends MediaItem {
private String director;
public Movie(String title, int durationMinutes, String director) {
super(title, durationMinutes);
this.director = director;
}
@Override
public void displayDetails() {
System.out.println("--- Movie ---");
System.out.println("Title: " + title);
System.out.println("Director: " + director);
System.out.println("Duration: " + durationMinutes + " minutes");
}
public void playMovie() {
System.out.println("Playing movie: " + title);
}
}
// Subclass: Book
class Book extends MediaItem {
private String author;
public Book(String title, int durationMinutes, String author) {
super(title, durationMinutes);
this.author = author;
}
@Override
public void displayDetails() {
System.out.println("--- Book ---");
System.out.println("Title: " + title);
System.out.println("Author: " + author);
System.out.println("Approx. Reading Time: " + durationMinutes + " minutes");
}
public void readBook() {
System.out.println("Reading book: " + title);
}
}
public class MediaItemPolymorphismExerciseSolution {
public static void main(String[] args) {
// Create a list to hold various MediaItems
List<MediaItem> library = new ArrayList<>();
// Add Movie and Book objects to the list, treating them as MediaItem
library.add(new Movie("Inception", 148, "Christopher Nolan"));
library.add(new Book("The Lord of the Rings", 1500, "J.R.R. Tolkien"));
library.add(new Movie("The Matrix", 136, "Lana Wachowski, Lilly Wachowski"));
library.add(new Book("1984", 400, "George Orwell"));
System.out.println("--- Displaying all library items ---");
for (MediaItem item : library) {
item.displayDetails(); // Polymorphic call: calls Movie's or Book's displayDetails()
System.out.println(); // Spacing
}
System.out.println("\n--- Demonstrating type-specific actions ---");
// We cannot call playMovie() or readBook() directly on 'item' from the loop
// because 'item' is of type MediaItem, which doesn't have those methods.
// We'll learn how to do this safely with casting in the next chapter.
}
}
Chapter 12: Casting Objects & Instanceof
Quick Theory:
While polymorphism allows us to treat objects of different types uniformly through a common superclass or interface reference (upcasting), sometimes we need to access methods or attributes specific to the original, more specialized class. This is where casting comes in, specifically downcasting (casting a superclass reference to a subclass type). Downcasting allows us to temporarily treat a generalized object as its more specific type, enabling access to its unique functionalities. However, downcasting is inherently risky because if the object is not actually an instance of the target subclass, a ClassCastException will occur at runtime.
To safely perform downcasting, Java provides the instanceof operator. This operator checks if an object is an instance of a particular class or an interface. It returns true if the object is compatible with the specified type, and false otherwise. By using instanceof before attempting a downcast, we ensure that the cast is valid, which significantly improves code robustness and prevents runtime errors. Java 16+ also introduced instanceof pattern matching, simplifying the syntax for this common pattern, enhancing flexibility and reusability of safe type checks.
Professional Code:
// Reusing Animal hierarchy from Chapter 11
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() { return name; }
public void makeSound() {
System.out.println(name + " makes a generic animal sound.");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " barks: Woof! Woof!");
}
public void fetch() {
System.out.println(getName() + " fetches the stick.");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " meows: Meow!");
}
public void purr() {
System.out.println(getName() + " purrs softly.");
}
}
// Example 1: Demonstrating safe downcasting with instanceof
import java.util.ArrayList;
import java.util.List;
public class CastingAndInstanceofDemo {
public static void main(String[] args) {
List<Animal> pets = new ArrayList<>();
pets.add(new Dog("Buddy"));
pets.add(new Cat("Whiskers"));
pets.add(new Dog("Lucy"));
pets.add(new Animal("Unknown pet")); // A generic Animal
System.out.println("--- Iterating and performing type-specific actions ---");
for (Animal pet : pets) {
pet.makeSound(); // Polymorphic call
// Using instanceof to safely check the type before downcasting
if (pet instanceof Dog) {
// Downcast: Treat the 'Animal' reference 'pet' as a 'Dog'
Dog dog = (Dog) pet;
dog.fetch(); // Now we can call Dog-specific methods
} else if (pet instanceof Cat) {
// Downcast: Treat the 'Animal' reference 'pet' as a 'Cat'
Cat cat = (Cat) pet;
cat.purr(); // Now we can call Cat-specific methods
} else {
System.out.println(pet.getName() + " is just a generic animal.");
}
System.out.println(); // Spacing
}
// --- Unsafe Casting Example (will cause ClassCastException) ---
System.out.println("--- Demonstrating unsafe casting (will crash) ---");
Animal genericAnimal = new Animal("Lion");
// This line would cause a ClassCastException at runtime
// because a generic Animal cannot be cast to a Dog.
// Dog potentialDog = (Dog) genericAnimal;
// potentialDog.fetch(); // This line would never be reached
System.out.println("If the above commented lines were active, a ClassCastException would occur.");
}
}
// Example 2: instanceof Pattern Matching (Java 16+)
// This simplifies the 'if (obj instanceof Type) { Type castedObj = (Type) obj; }' pattern.
import java.util.ArrayList;
import java.util.List;
public class InstanceofPatternMatchingDemo {
public static void processAnimal(Animal animal) {
animal.makeSound();
// Old way (pre-Java 16):
// if (animal instanceof Dog) {
// Dog dog = (Dog) animal;
// dog.fetch();
// }
// New way (Java 16+): instanceof Pattern Matching
// If 'animal' is a Dog, it's automatically cast to 'dog' for the scope of the if block.
if (animal instanceof Dog dog) {
dog.fetch(); // 'dog' is directly available and already cast.
} else if (animal instanceof Cat cat) {
cat.purr(); // 'cat' is directly available and already cast.
} else {
System.out.println(animal.getName() + " cannot perform specific actions.");
}
}
public static void main(String[] args) {
System.out.println("--- Instanceof Pattern Matching Demo (Java 16+) ---");
processAnimal(new Dog("Rex"));
System.out.println();
processAnimal(new Cat("Mittens"));
System.out.println();
processAnimal(new Animal("Zebra"));
}
}
Clean Code Tip:
Minimize downcasting as much as possible. If you frequently find yourself using instanceof checks followed by downcasts, it often indicates a potential design flaw. Consider whether polymorphism (by moving the specific method up to the superclass or an interface) or the Visitor pattern could provide a more elegant and extensible solution, reducing the need for explicit type checks.
Exercise:
Reuse the MediaItem, Movie, and Book classes from the previous chapter. Create an ArrayList<MediaItem>. Add a Movie and a Book object. Iterate through the list. Inside the loop, use instanceof to check if the current MediaItem is a Movie or a Book. If it's a Movie, downcast it and call its playMovie() method. If it's a Book, downcast it and call its readBook() method.
Solution:
import java.util.ArrayList;
import java.util.List;
// Reusing MediaItem hierarchy
abstract class MediaItem {
protected String title;
protected int durationMinutes;
public MediaItem(String title, int durationMinutes) {
this.title = title;
this.durationMinutes = durationMinutes;
}
public String getTitle() {
return title;
}
public abstract void displayDetails();
}
class Movie extends MediaItem {
private String director;
public Movie(String title, int durationMinutes, String director) {
super(title, durationMinutes);
this.director = director;
}
@Override
public void displayDetails() {
System.out.println("Movie: " + title + " (Dir: " + director + ", " + durationMinutes + " min)");
}
public void playMovie() {
System.out.println("Now playing: " + title + " by " + director);
}
}
class Book extends MediaItem {
private String author;
public Book(String title, int durationMinutes, String author) {
super(title, durationMinutes);
this.author = author;
}
@Override
public void displayDetails() {
System.out.println("Book: " + title + " (Auth: " + author + ", ~" + durationMinutes + " min read)");
}
public void readBook() {
System.out.println("Now reading: " + title + " by " + author);
}
}
public class MediaItemCastingExerciseSolution {
public static void main(String[] args) {
List<MediaItem> myCollection = new ArrayList<>();
myCollection.add(new Movie("Interstellar", 169, "Christopher Nolan"));
myCollection.add(new Book("Dune", 750, "Frank Herbert"));
myCollection.add(new Movie("Arrival", 116, "Denis Villeneuve"));
myCollection.add(new Book("The Alchemist", 200, "Paulo Coelho"));
myCollection.add(new MediaItem("Generic Item", 0) { // Anonymous inner class for an unknown media type
@Override
public void displayDetails() {
System.out.println("Unknown Media Item: " + title);
}
});
System.out.println("--- Processing Media Collection ---");
for (MediaItem item : myCollection) {
item.displayDetails(); // Polymorphic call
// Using instanceof (with pattern matching for Java 16+)
if (item instanceof Movie movie) {
movie.playMovie(); // Call Movie-specific method
} else if (item instanceof Book book) {
book.readBook(); // Call Book-specific method
} else {
System.out.println("Cannot perform specific action for: " + item.getTitle());
}
System.out.println(); // Spacing
}
}
}
Chapter 13: ArrayList Foundations
Quick Theory:
When dealing with collections of data in programming, a fundamental decision involves choosing between fixed-size arrays and dynamic lists. Fixed-size arrays, like String[] names = new String[10], require you to specify their size at the time of creation. This can become a significant limitation if the number of elements you need to store changes during program execution, forcing you to manually create new, larger arrays and copy elements, which is cumbersome and error-prone.
The Java Collections Framework provides dynamic lists, with ArrayList being the most commonly used, to overcome these limitations. ArrayLists automatically resize themselves as elements are added or removed, offering immense flexibility and ease of use. They are built on top of arrays but abstract away the resizing mechanism, allowing developers to focus on managing data rather than array capacity, thereby significantly enhancing code reusability and maintainability.
Professional Code:
import java.util.ArrayList;
import java.util.List; // Good practice to program to the interface
// Custom object to store in our ArrayList
class Product {
String name;
double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
// A useful toString() method for easy printing
@Override
public String toString() {
return "Product [Name: " + name + ", Price: $" + String.format("%.2f", price) + "]";
}
}
// Example 1: Creating an ArrayList and adding elements
public class ArrayListAddDemo {
public static void main(String[] args) {
// Declare a List reference, initialize with ArrayList.
// The type parameter <Product> specifies that this list will hold Product objects.
// This is generic programming, ensuring type safety.
List<Product> shoppingCart = new ArrayList<>();
System.out.println("--- Initial Shopping Cart ---");
System.out.println("Is cart empty? " + shoppingCart.isEmpty()); // Check if the list is empty
System.out.println("Cart size: " + shoppingCart.size()); // Get current number of elements (0)
// Adding Product objects to the ArrayList.
// The 'add()' method appends the element to the end of the list.
shoppingCart.add(new Product("Laptop", 1200.00));
shoppingCart.add(new Product("Mouse", 25.50));
shoppingCart.add(new Product("Keyboard", 75.00));
System.out.println("\n--- Shopping Cart After Adding Items ---");
System.out.println("Cart size: " + shoppingCart.size()); // Current number of elements (3)
System.out.println("Is cart empty? " + shoppingCart.isEmpty());
System.out.println("Contents: " + shoppingCart); // Prints the list using elements' toString()
}
}
// Example 2: Accessing and Updating Elements, size() vs length
public class ArrayListAccessUpdateDemo {
public static void main(String[] args) {
List<Product> inventory = new ArrayList<>();
inventory.add(new Product("Monitor", 300.00));
inventory.add(new Product("Webcam", 50.00));
inventory.add(new Product("Headphones", 100.00));
System.out.println("--- Initial Inventory ---");
System.out.println("Inventory size: " + inventory.size());
System.out.println(inventory);
// Accessing elements by index using get(index).
// Indices are 0-based, just like arrays.
Product firstProduct = inventory.get(0);
System.out.println("\nFirst product: " + firstProduct.name);
Product thirdProduct = inventory.get(2);
System.out.println("Third product: " + thirdProduct.name);
// Updating an element at a specific index using set(index, element).
// This replaces the existing element at that index.
System.out.println("\n--- Updating Second Product ---");
Product updatedWebcam = new Product("HD Webcam Pro", 75.00);
inventory.set(1, updatedWebcam); // Replace "Webcam" with "HD Webcam Pro"
System.out.println("Inventory after update: " + inventory);
System.out.println("Inventory size: " + inventory.size());
// Key difference: ArrayList uses size() method for current element count.
// Arrays use the .length field for their fixed capacity.
// int[] fixedArray = new int[5];
// System.out.println(fixedArray.length); // 5 (capacity)
// System.out.println(inventory.size()); // 3 (actual elements)
}
}
// Example 3: Removing Elements from an ArrayList
public class ArrayListRemoveDemo {
public static void main(String[] args) {
List<Product> cart = new ArrayList<>();
Product laptop = new Product("Laptop", 1200.00);
Product mouse = new Product("Mouse", 25.50);
Product keyboard = new Product("Keyboard", 75.00);
Product monitor = new Product("Monitor", 300.00);
cart.add(laptop);
cart.add(mouse);
cart.add(keyboard);
cart.add(monitor);
System.out.println("--- Initial Cart ---");
System.out.println("Size: " + cart.size());
System.out.println(cart);
// Removing by index: Removes the element at the specified position.
// All subsequent elements shift left (index decreases by 1).
System.out.println("\n--- Removing item at index 1 (Mouse) ---");
Product removedByIndex = cart.remove(1); // Removes mouse
System.out.println("Removed: " + removedByIndex.name);
System.out.println("Size: " + cart.size());
System.out.println(cart); // Now Laptop, Keyboard, Monitor
// Removing by object: Removes the first occurrence of the specified object.
// For custom objects, this relies on the `equals()` method.
// If `equals()` is not overridden, it checks for memory address equality.
System.out.println("\n--- Removing 'keyboard' object ---");
boolean removedByObject = cart.remove(keyboard); // Removes keyboard
System.out.println("Was Keyboard removed? " + removedByObject);
System.out.println("Size: " + cart.size());
System.out.println(cart); // Now Laptop, Monitor
// Trying to remove an object not in the list
System.out.println("\n--- Trying to remove 'NonExistent' product ---");
boolean removedNonExistent = cart.remove(new Product("NonExistent", 0.0));
System.out.println("Was NonExistent removed? " + removedNonExistent); // False
System.out.println("Size: " + cart.size());
System.out.println(cart);
// Clearing all elements from the list
System.out.println("\n--- Clearing the entire cart ---");
cart.clear();
System.out.println("Size after clear: " + cart.size());
System.out.println("Is cart empty? " + cart.isEmpty());
}
}
Clean Code Tip:
When choosing a collection type, opt for a List (like ArrayList) when the order of elements is important, and duplicate elements are allowed. If you don't need random access by index, using List as the interface type (List<Product> products = new ArrayList<>();) is good practice, as it provides flexibility to switch to other List implementations later (e.g., LinkedList) without altering surrounding code.
Exercise:
Create a Student class with studentId (String) and name (String). Create an ArrayList<Student>. Add three Student objects. Display the initial list, then remove one student by studentId (you'll need to manually iterate to find and remove), and finally, display the updated list and its size.
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator; // Needed for safe removal during iteration
class Student {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
// toString() for easy printing
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
// Override equals() and hashCode() if we want to remove by object reference,
// or if we ever put Students in a Set or as keys in a Map.
// For this exercise, we'll manually iterate to find by ID.
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Student student = (Student) obj;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return studentId.hashCode();
}
}
public class StudentListExerciseSolution {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("S001", "Alice Smith"));
students.add(new Student("S002", "Bob Johnson"));
students.add(new Student("S003", "Charlie Brown"));
System.out.println("--- Initial Student List (Size: " + students.size() + ") ---");
for (Student s : students) {
System.out.println(s);
}
// Task: Remove student with ID "S002"
String idToRemove = "S002";
System.out.println("\n--- Attempting to remove student with ID: " + idToRemove + " ---");
// Iterate safely using an Iterator to remove elements.
// Modifying a list during an enhanced for-loop will throw ConcurrentModificationException.
Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()) {
Student currentStudent = iterator.next();
if (currentStudent.studentId.equals(idToRemove)) {
iterator.remove(); // Safely removes the current element
System.out.println("Removed: " + currentStudent.name);
break; // Assuming IDs are unique, we can stop after finding.
}
}
System.out.println("\n--- Updated Student List (Size: " + students.size() + ") ---");
for (Student s : students) {
System.out.println(s);
}
}
}
Chapter 14: Sorting Collections
Quick Theory:
Sorting collections is a common and essential task in software development, whether for presenting data in a logical order, optimizing search operations, or preparing data for other algorithms. While simple data types like String and Integer have a natural ordering that Java understands implicitly, sorting custom objects (like Book by title or price) requires explicit instructions on how to compare them.
The Java Collections Framework provides powerful tools for sorting, notably the Collections.sort() method. To enable sorting of custom objects, you typically use one of two interfaces: Comparable or Comparator. The Comparable interface allows a class to define its "natural ordering" (e.g., a Book knows how to compare itself to another Book based on its title), while the Comparator interface provides a way to define multiple custom sort orders, external to the class itself, offering greater flexibility and reusability for different sorting needs.
Professional Code:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; // For custom sorting logic
import java.util.List;
// Custom object: Book class
// To sort Books by their natural order (e.g., title), we make it Comparable.
class Book implements Comparable<Book> {
String title;
String author;
int pages;
double price;
public Book(String title, String author, int pages, double price) {
this.title = title;
this.author = author;
this.pages = pages;
this.price = price;
}
// Getters are good practice for private fields, but for simplicity here,
// we'll access them directly.
public String getTitle() { return title; }
public String getAuthor() { return author; }
public int getPages() { return pages; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Book [Title: '" + title + "', Author: '" + author + "', Pages: " + pages + ", Price: $" + String.format("%.2f", price) + "]";
}
// Implementing Comparable interface defines the "natural ordering" for Book objects.
// Here, we define it to sort books alphabetically by title.
@Override
public int compareTo(Book otherBook) {
// String's compareTo() method provides lexicographical comparison.
// It returns:
// - a negative integer if this.title comes before otherBook.title
// - zero if they are equal
// - a positive integer if this.title comes after otherBook.title
return this.title.compareTo(otherBook.title);
}
}
// Example 1: Sorting a list of Strings (using natural order)
public class SimpleSortingDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Charlie");
names.add("Alice");
names.add("Bob");
names.add("Diana");
System.out.println("--- Unsorted Names ---");
System.out.println(names);
// Collections.sort() uses the natural order for String (alphabetical).
// Strings naturally implement Comparable<String>.
Collections.sort(names);
System.out.println("\n--- Sorted Names ---");
System.out.println(names);
}
}
// Example 2: Sorting a list of custom Objects using Comparable (natural order)
public class CustomObjectSortingComparableDemo {
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 193, 12.99));
books.add(new Book("1984", "George Orwell", 328, 9.50));
books.add(new Book("Brave New World", "Aldous Huxley", 311, 10.75));
books.add(new Book("To Kill a Mockingbird", "Harper Lee", 281, 8.99));
System.out.println("--- Unsorted Books ---");
for (Book book : books) {
System.out.println(book);
}
// Collections.sort() will now use the compareTo() method defined in our Book class
// to sort books by title.
Collections.sort(books);
System.out.println("\n--- Books Sorted by Title (Natural Order - Comparable) ---");
for (Book book : books) {
System.out.println(book);
}
}
}
// Example 3: Sorting a list of custom Objects using Comparator (custom order)
public class CustomObjectSortingComparatorDemo {
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 193, 12.99));
books.add(new Book("1984", "George Orwell", 328, 9.50));
books.add(new Book("Brave New World", "Aldous Huxley", 311, 10.75));
books.add(new Book("To Kill a Mockingbird", "Harper Lee", 281, 8.99));
books.add(new Book("Dune", "Frank Herbert", 412, 14.99));
System.out.println("--- Unsorted Books ---");
for (Book book : books) {
System.out.println(book);
}
// --- Sorting by Price using a Comparator ---
// A Comparator is a separate class (or anonymous/lambda) that defines a comparison logic.
// It allows sorting by criteria other than the natural order.
Comparator<Book> priceComparator = new Comparator<Book>() {
@Override
public int compare(Book b1, Book b2) {
// Compare books based on their price.
// Double.compare handles floating-point precision issues correctly.
return Double.compare(b1.getPrice(), b2.getPrice());
}
};
// Sort using the custom Comparator.
Collections.sort(books, priceComparator);
System.out.println("\n--- Books Sorted by Price (Custom Order - Comparator) ---");
for (Book book : books) {
System.out.println(book);
}
// --- Sorting by Pages using a Lambda Comparator (Java 8+) ---
// Lambdas provide a concise way to create single-method interfaces like Comparator.
// This sorts in descending order of pages.
Collections.sort(books, (b1, b2) -> Integer.compare(b2.getPages(), b1.getPages()));
System.out.println("\n--- Books Sorted by Pages (Descending - Lambda Comparator) ---");
for (Book book : books) {
System.out.println(book);
}
}
}
Clean Code Tip:
Use the Comparable interface when your class has one obvious, "natural" way to order its objects (e.g., sorting Person by lastName). Use the Comparator interface when you need to define multiple different sorting criteria, or when you cannot modify the class you want to sort. This provides maximum flexibility for varied sorting requirements.
Exercise:
Reuse your Student class from Chapter 13 (with studentId and name). Make the Student class Comparable so that students are naturally sorted alphabetically by their name. Create an ArrayList<Student>, add several students (some out of order), and then use Collections.sort() to sort them. Print the sorted list.
Solution:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// Student class now implements Comparable<Student>
class Student implements Comparable<Student> {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
public String getStudentId() { return studentId; }
public String getName() { return name; }
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
// Define the natural order: sort by student name alphabetically.
@Override
public int compareTo(Student otherStudent) {
return this.name.compareTo(otherStudent.name);
}
// Necessary for equality checks if ever used in Set/Map keys
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return studentId.hashCode();
}
}
public class StudentSortingExerciseSolution {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("S003", "Charlie Brown"));
students.add(new Student("S001", "Alice Smith"));
students.add(new Student("S004", "David Lee"));
students.add(new Student("S002", "Bob Johnson"));
System.out.println("--- Unsorted Student List ---");
for (Student s : students) {
System.out.println(s);
}
// Use Collections.sort() to sort the list.
// It will use the compareTo() method defined in the Student class.
Collections.sort(students);
System.out.println("\n--- Sorted Student List (by Name) ---");
for (Student s : students) {
System.out.println(s);
}
// Example of sorting by ID using a Comparator (lambda)
System.out.println("\n--- Sorted Student List (by ID using Comparator) ---");
Collections.sort(students, (s1, s2) -> s1.getStudentId().compareTo(s2.getStudentId()));
for (Student s : students) {
System.out.println(s);
}
}
}
Chapter 15: The HashSet (Unique Data)
Quick Theory:
In many scenarios, you need a collection that guarantees uniqueness among its elements. For example, a list of registered users where each username must be distinct, or a set of tags for an article where each tag appears only once. While an ArrayList allows duplicates, manually checking for uniqueness before every insertion is inefficient and prone to errors.
The Set interface, a core part of the Java Collections Framework, is specifically designed for collections that contain no duplicate elements. HashSet, a common implementation of Set, offers highly efficient addition, removal, and lookup operations (typically constant time, O(1), on average). It achieves this speed by using a hash table under the hood. For custom objects to work correctly in a HashSet (i.e., for uniqueness and lookups to function as expected), it is critical to properly override both the equals() and hashCode() methods in your custom class. Without them, HashSet might consider two semantically identical objects as distinct due to different memory addresses, violating the uniqueness contract.
Professional Code:
import java.util.HashSet;
import java.util.Set; // Program to the interface
// Custom object: User class
// Crucially, equals() and hashCode() must be overridden for HashSet to work correctly.
class User {
int id;
String username;
String email;
public User(int id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
public int getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
@Override
public String toString() {
return "User [ID: " + id + ", Username: '" + username + "', Email: '" + email + "']";
}
// --- CRITICAL for HashSet: Override equals() and hashCode() ---
// Two User objects are considered equal if they have the same ID.
@Override
public boolean equals(Object o) {
// If the objects are the same instance, they are equal.
if (this == o) return true;
// If the other object is null or not of the same class, they are not equal.
if (o == null || getClass() != o.getClass()) return false;
// Cast the object to User type.
User user = (User) o;
// Compare based on the 'id' field for equality.
return id == user.id;
}
// hashCode() must be consistent with equals().
// If two objects are equal according to the equals(Object) method,
// then calling the hashCode method on each of the two objects must produce the same integer result.
@Override
public int hashCode() {
// Use a utility from Objects class for better hashCode generation.
// return Objects.hash(id); // Requires java.util.Objects
// A simpler way for a single int field:
return Integer.hashCode(id);
}
}
// Example 1: Demonstrating HashSet uniqueness and adding elements
public class HashSetUniquenessDemo {
public static void main(String[] args) {
Set<User> uniqueUsers = new HashSet<>();
User user1 = new User(1, "alice.smith", "alice@example.com");
User user2 = new User(2, "bob.builder", "bob@example.com");
User user3 = new User(1, "alice.smith.duplicate", "alice_dup@example.com"); // Same ID as user1
System.out.println("--- Adding Users to HashSet ---");
System.out.println("Added user1: " + uniqueUsers.add(user1)); // true (first addition)
System.out.println("Added user2: " + uniqueUsers.add(user2)); // true
System.out.println("Added user3 (duplicate ID): " + uniqueUsers.add(user3)); // false (rejected due to user1's ID and equals/hashCode)
System.out.println("\n--- Current Users in Set (Size: " + uniqueUsers.size() + ") ---");
// The output order of elements in a HashSet is not guaranteed due to hashing.
for (User user : uniqueUsers) {
System.out.println(user);
}
// Note: Even though user3 had a different username/email, because its ID was 1,
// and User.equals() checks by ID, it was considered a duplicate of user1.
}
}
// Example 2: Checking for existence and removing elements from HashSet
public class HashSetOperationsDemo {
public static void main(String[] args) {
Set<User> activeUsers = new HashSet<>();
activeUsers.add(new User(101, "admin", "admin@domain.com"));
activeUsers.add(new User(102, "moderator", "mod@domain.com"));
activeUsers.add(new User(103, "guest", "guest@domain.com"));
System.out.println("--- Initial Active Users (Size: " + activeUsers.size() + ") ---");
System.out.println(activeUsers);
// Checking for existence using contains().
// This is very fast (average O(1)) in a HashSet.
User searchUser1 = new User(102, "moderator", "mod@domain.com"); // ID 102
System.out.println("\nDoes set contain user with ID 102? " + activeUsers.contains(searchUser1)); // true
User searchUser2 = new User(999, "nonexistent", "none@domain.com"); // ID 999
System.out.println("Does set contain user with ID 999? " + activeUsers.contains(searchUser2)); // false
// Removing an element. Relies on equals() and hashCode() as well.
System.out.println("\n--- Removing user with ID 103 ---");
User userToRemove = new User(103, "guest", "guest@domain.com"); // ID 103
System.out.println("Removed user with ID 103? " + activeUsers.remove(userToRemove)); // true
System.out.println("--- Active Users After Removal (Size: " + activeUsers.size() + ") ---");
System.out.println(activeUsers);
// Trying to remove a non-existent user
System.out.println("\n--- Trying to remove non-existent user ---");
User userNonExistent = new User(105, "somebody", "somebody@domain.com");
System.out.println("Removed user with ID 105? " + activeUsers.remove(userNonExistent)); // false
System.out.println("--- Final Active Users (Size: " + activeUsers.size() + ") ---");
System.out.println(activeUsers);
}
}
Clean Code Tip:
Whenever you use custom objects in hash-based collections like HashSet or HashMap (as keys), it is absolutely critical to correctly override both the equals() and hashCode() methods. equals() defines when two objects are semantically the same, and hashCode() provides a hash code consistent with equals(). Failing to do so will lead to unexpected behavior, such as duplicate elements being added to a Set or Map keys not being found, breaking the collection's contract.
Exercise:
Create a Course class with courseCode (String) and title (String). Override equals() and hashCode() so that two Course objects are considered equal if they have the same courseCode. Create a HashSet<Course>. Add a few unique courses, then try to add a course with a duplicate courseCode but a different title. Display the HashSet and its final size to observe the uniqueness enforcement.
Solution:
import java.util.HashSet;
import java.util.Objects; // For Objects.hash()
import java.util.Set;
class Course {
String courseCode;
String title;
public Course(String courseCode, String title) {
this.courseCode = courseCode;
this.title = title;
}
public String getCourseCode() { return courseCode; }
public String getTitle() { return title; }
@Override
public String toString() {
return "Course [Code: " + courseCode + ", Title: '" + title + "']";
}
// Override equals() based on courseCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Course course = (Course) o;
return Objects.equals(courseCode, course.courseCode); // Compare courseCode for equality
}
// Override hashCode() consistent with equals()
@Override
public int hashCode() {
return Objects.hash(courseCode); // Hash based on courseCode
}
}
public class CourseHashSetExerciseSolution {
public static void main(String[] args) {
Set<Course> availableCourses = new HashSet<>();
Course javaCourse1 = new Course("CS101", "Introduction to Java");
Course pythonCourse = new Course("CS102", "Python for Beginners");
Course dataStructuresCourse = new Course("CS201", "Data Structures & Algorithms");
System.out.println("--- Adding Courses ---");
System.out.println("Added CS101: " + availableCourses.add(javaCourse1));
System.out.println("Added CS102: " + availableCourses.add(pythonCourse));
System.out.println("Added CS201: " + availableCourses.add(dataStructuresCourse));
// Attempt to add a course with a duplicate courseCode but different title
Course javaCourse2_duplicateCode = new Course("CS101", "Advanced Java Programming");
System.out.println("Attempted to add CS101 (Advanced Java): " + availableCourses.add(javaCourse2_duplicateCode)); // This should be false
System.out.println("\n--- Final Available Courses (Size: " + availableCourses.size() + ") ---");
for (Course course : availableCourses) {
System.out.println(course);
}
// Expected output: only 3 courses because "CS101" is treated as a duplicate.
// The exact "CS101" object in the set depends on insertion order and internal hashing.
}
}
Chapter 16: HashMap (Key-Value Pairs)
Quick Theory: Many real-world data structures rely on associating a unique identifier (a key) with a specific piece of information (a value). Think of a dictionary (word -> definition), a phone book (name -> phone number), or a registry (ID -> User details). While lists allow you to store collections of items, retrieving a specific item by an arbitrary unique identifier (other than its index) requires iterating through the entire list, which becomes inefficient for large datasets.
The Map interface, specifically its HashMap implementation, is the answer to this problem in Java. A HashMap stores data as key-value pairs, where each key is unique and maps to exactly one value. This structure allows for incredibly fast lookups (average O(1) time) when retrieving a value using its key. This makes HashMap the most important and frequently used collection when you need efficient access to data based on a unique identifier, significantly boosting reusability and flexibility in data management. Similar to HashSet, for custom objects used as keys, correctly overriding equals() and hashCode() is paramount.
Professional Code:
import java.util.HashMap;
import java.util.Map; // Program to the interface
// Custom object to store as values in our HashMap
class Order {
String orderId;
String customerName;
double totalAmount;
String status;
public Order(String orderId, String customerName, double totalAmount, String status) {
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
this.status = status;
}
public String getOrderId() { return orderId; }
public String getCustomerName() { return customerName; }
public double getTotalAmount() { return totalAmount; }
public String getStatus() { return status; }
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "Order [ID: " + orderId + ", Customer: '" + customerName + "', Total: $" + String.format("%.2f", totalAmount) + ", Status: " + status + "]";
}
// If Order objects were to be used as KEYS in a HashMap, we would also need
// to override equals() and hashCode() based on 'orderId'.
// For this example, we use String keys, so Order's default equals/hashCode are fine.
}
// Example 1: Creating a HashMap and using put()
public class HashMapPutDemo {
public static void main(String[] args) {
// Create a HashMap where String (orderId) is the key and Order object is the value.
Map<String, Order> orders = new HashMap<>();
System.out.println("--- Initial Orders Map (Size: " + orders.size() + ") ---");
System.out.println("Is map empty? " + orders.isEmpty());
// Using put(key, value) to add entries.
// If the key already exists, put() replaces the old value with the new one
// and returns the old value. Otherwise, it returns null.
orders.put("ORD001", new Order("ORD001", "Alice Wonderland", 150.75, "Pending"));
orders.put("ORD002", new Order("ORD002", "Bob Builder", 230.00, "Shipped"));
orders.put("ORD003", new Order("ORD003", "Charlie Chaplin", 75.20, "Delivered"));
System.out.println("\n--- Orders Map After Adding (Size: " + orders.size() + ") ---");
System.out.println(orders); // Prints the map using toString() of its entries
// Adding an order with an existing key (ORD002) - this will update the value.
System.out.println("\n--- Updating Order ORD002 ---");
Order oldOrder = orders.put("ORD002", new Order("ORD002", "Bob Builder", 250.00, "Processing"));
System.out.println("Old order for ORD002 was: " + oldOrder);
System.out.println("Current orders: " + orders); // ORD002 value is now updated
}
}
// Example 2: Using get(), containsKey(), remove() with HashMap
public class HashMapOperationsDemo {
public static void main(String[] args) {
Map<String, Order> currentOrders = new HashMap<>();
currentOrders.put("A101", new Order("A101", "John Doe", 50.00, "Pending"));
currentOrders.put("B202", new Order("B202", "Jane Smith", 120.50, "Shipped"));
currentOrders.put("C303", new Order("C303", "Peter Jones", 200.00, "Delivered"));
System.out.println("--- Current Orders Map (Size: " + currentOrders.size() + ") ---");
System.out.println(currentOrders);
// Retrieving a value using get(key). Returns null if key not found.
String searchKey1 = "B202";
Order orderB202 = currentOrders.get(searchKey1);
if (orderB202 != null) {
System.out.println("\nRetrieved order " + searchKey1 + ": " + orderB202.getCustomerName());
orderB202.setStatus("In Transit"); // Modify the retrieved object
System.out.println("Updated status for B202: " + currentOrders.get(searchKey1).getStatus());
} else {
System.out.println("\nOrder " + searchKey1 + " not found.");
}
String searchKey2 = "D404";
Order orderD404 = currentOrders.get(searchKey2);
System.out.println("Retrieved order " + searchKey2 + ": " + orderD404); // null
// Checking if a key exists using containsKey(key).
System.out.println("\nDoes map contain key 'A101'? " + currentOrders.containsKey("A101")); // true
System.out.println("Does map contain key 'X999'? " + currentOrders.containsKey("X999")); // false
// Removing an entry using remove(key). Returns the value associated with the key.
System.out.println("\n--- Removing order 'C303' ---");
Order removedOrder = currentOrders.remove("C303");
if (removedOrder != null) {
System.out.println("Removed order: " + removedOrder);
} else {
System.out.println("Order 'C303' not found for removal.");
}
System.out.println("Map after removal (Size: " + currentOrders.size() + "): " + currentOrders);
// Clear all entries
currentOrders.clear();
System.out.println("\nMap cleared. Size: " + currentOrders.size());
}
}
// Example 3: Iterating over a HashMap
import java.util.Set;
public class HashMapIterationDemo {
public static void main(String[] args) {
Map<String, Order> orders = new HashMap<>();
orders.put("ORD001", new Order("ORD001", "Alice", 150.75, "Pending"));
orders.put("ORD002", new Order("ORD002", "Bob", 230.00, "Shipped"));
orders.put("ORD003", new Order("ORD003", "Charlie", 75.20, "Delivered"));
System.out.println("--- Iterating over Keys ---");
// Get a Set of all keys in the map.
Set<String> orderIds = orders.keySet();
for (String id : orderIds) {
Order order = orders.get(id); // Retrieve value using key
System.out.println("Order ID: " + id + ", Customer: " + order.getCustomerName());
}
System.out.println("\n--- Iterating over Values ---");
// Get a Collection of all values in the map.
// Note: The Collection of values can contain duplicates if different keys map to identical values.
for (Order order : orders.values()) {
System.out.println("Order Status: " + order.getStatus() + ", Total: $" + String.format("%.2f", order.getTotalAmount()));
}
System.out.println("\n--- Iterating over Key-Value Pairs (EntrySet) ---");
// The most efficient way to iterate, as it gives you both key and value
// without an extra lookup (orders.get(id)).
for (Map.Entry<String, Order> entry : orders.entrySet()) {
String id = entry.getKey();
Order order = entry.getValue();
System.out.println("Entry -> Key: " + id + ", Value: " + order);
}
}
}
Clean Code Tip:
Choose a Map (specifically HashMap for general use) when you need to store data as key-value pairs and quickly retrieve values using a unique identifier. This is ideal for scenarios like user registries, caching data by ID, or configuration settings. For custom objects used as keys, remember the equals() and hashCode() rule.
Exercise:
Reuse your Student class (studentId, name). Create a HashMap<String, Student> where studentId is the key and the Student object is the value. Add three Student objects to the map. Then, retrieve and print the name of the student with a specific studentId. Check if another studentId exists in the map. Finally, remove one student by studentId and print the map's final size.
Solution:
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
// Reusing Student class (with proper equals/hashCode for future map key usage)
class Student {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
public String getStudentId() { return studentId; }
public String getName() { return name; }
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
// Crucial if Student objects were to be used as keys in a HashMap/HashSet
// Here, studentId is our primary key.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return Objects.hash(studentId);
}
}
public class StudentHashMapExerciseSolution {
public static void main(String[] args) {
// HashMap where key is studentId (String) and value is a Student object
Map<String, Student> studentRegistry = new HashMap<>();
// Add students
studentRegistry.put("S001", new Student("S001", "Alice Smith"));
studentRegistry.put("S002", new Student("S002", "Bob Johnson"));
studentRegistry.put("S003", new Student("S003", "Charlie Brown"));
System.out.println("--- Initial Student Registry (Size: " + studentRegistry.size() + ") ---");
System.out.println(studentRegistry);
// Retrieve student by ID "S002"
String studentIdToFind = "S002";
Student foundStudent = studentRegistry.get(studentIdToFind);
if (foundStudent != null) {
System.out.println("\nFound Student with ID " + studentIdToFind + ": " + foundStudent.getName());
} else {
System.out.println("\nStudent with ID " + studentIdToFind + " not found.");
}
// Check if student ID "S004" exists
String nonExistentId = "S004";
System.out.println("Does student ID " + nonExistentId + " exist? " + studentRegistry.containsKey(nonExistentId));
// Remove student with ID "S001"
String studentIdToRemove = "S001";
System.out.println("\n--- Removing student with ID: " + studentIdToRemove + " ---");
Student removedStudent = studentRegistry.remove(studentIdToRemove);
if (removedStudent != null) {
System.out.println("Removed: " + removedStudent.getName());
} else {
System.out.println("Student with ID " + studentIdToRemove + " not found for removal.");
}
System.out.println("\n--- Final Student Registry (Size: " + studentRegistry.size() + ") ---");
System.out.println(studentRegistry);
}
}
Chapter 17: Iterating with Iterators & Streams
Quick Theory:
To process elements within a collection, you need to iterate over them. Traditionally, Java offered for loops (indexed or enhanced for-each loops) and the Iterator interface. The for-each loop is convenient for simply visiting each element, but it doesn't allow safe removal of elements from the collection during iteration. The Iterator provides explicit control over the iteration process, crucially offering a remove() method that safely modifies the underlying collection without triggering ConcurrentModificationExceptions.
With Java 8, the Streams API was introduced, providing a powerful, functional-style approach to processing collections. Streams allow you to declaratively define a sequence of operations (like filtering, mapping, and collecting) on elements, focusing on what to do rather than how to do it. This enhances both reusability (common operations as methods) and flexibility (chainable operations). While Iterator is good for modifying a collection during iteration, Streams are excellent for transforming or querying data without altering the original collection.
Professional Code:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors; // For collecting stream results
// Reusing Product class from Chapter 13
class Product {
String name;
double price;
boolean inStock;
public Product(String name, double price, boolean inStock) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isInStock() { return inStock; }
@Override
public String toString() {
return "Product [Name: " + name + ", Price: $" + String.format("%.2f", price) + ", In Stock: " + inStock + "]";
}
}
// Example 1: Basic Iteration with Enhanced For-Loop
public class ForEachIterationDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, true));
products.add(new Product("Mouse", 25.50, false)); // Out of stock
products.add(new Product("Keyboard", 75.00, true));
products.add(new Product("Webcam", 50.00, true));
products.add(new Product("Monitor", 300.00, false)); // Out of stock
System.out.println("--- All Products (Enhanced For-Loop) ---");
// The enhanced for-loop (for-each) is the simplest way to iterate
// when you only need to read elements.
for (Product p : products) {
System.out.println(p.getName() + " - " + (p.isInStock() ? "Available" : "Out of Stock"));
}
// Limitation: Cannot safely remove elements during this loop.
// The following line would cause a ConcurrentModificationException:
// for (Product p : products) { if (!p.isInStock()) products.remove(p); }
}
}
// Example 2: Iterating and Safely Removing with Iterator
public class IteratorRemoveDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, true));
products.add(new Product("Mouse", 25.50, false));
products.add(new Product("Keyboard", 75.00, true));
products.add(new Product("Webcam", 50.00, true));
products.add(new Product("Monitor", 300.00, false));
System.out.println("--- Initial Products ---");
System.out.println(products);
System.out.println("\n--- Removing Out-of-Stock Products using Iterator ---");
// An Iterator allows you to safely remove elements from the collection
// while iterating through it.
Iterator<Product> iterator = products.iterator();
while (iterator.hasNext()) { // Check if there's a next element
Product p = iterator.next(); // Get the next element
if (!p.isInStock()) {
System.out.println("Removing: " + p.getName());
iterator.remove(); // Safely remove the current element
}
}
System.out.println("\n--- Products After Removal ---");
System.out.println(products);
}
}
// Example 3: Iterating and Filtering with Streams (Java 8+)
public class StreamsDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, true));
products.add(new Product("Mouse", 25.50, false));
products.add(new Product("Keyboard", 75.00, true));
products.add(new Product("Webcam", 50.00, true));
products.add(new Product("Monitor", 300.00, false));
System.out.println("--- All Products (forEach Stream) ---");
// Using forEach with a Stream for simple iteration.
// Note: forEach is a terminal operation.
products.stream().forEach(p -> System.out.println(p.getName()));
System.out.println("\n--- Filtering In-Stock Products (Stream) ---");
// Stream operations are generally "lazy" and chainable.
// .filter() is an intermediate operation, producing a new stream.
// .collect() is a terminal operation, triggering the execution and gathering results.
List<Product> inStockProducts = products.stream()
.filter(Product::isInStock) // Method reference: equivalent to p -> p.isInStock()
.collect(Collectors.toList()); // Collect filtered products into a new List
System.out.println("In-stock products: " + inStockProducts);
System.out.println("\n--- Filtering Products by Price (Stream) ---");
// Find products cheaper than $100 and print their names
products.stream()
.filter(p -> p.getPrice() < 100.00) // Filter for price
.map(Product::getName) // Transform Product objects into their names (String)
.forEach(name -> System.out.println("Affordable item: " + name));
}
}
Clean Code Tip:
For simple, read-only iteration, use the enhanced for-each loop. If you need to safely remove elements from a collection during iteration, use an Iterator. For complex data transformations, filtering, and aggregation, leverage the Streams API for its declarative syntax and functional approach, which promotes more readable and often more performant code.
Exercise:
Reuse your Student class (studentId, name). Create an ArrayList<Student>. Add at least five Student objects, including some whose names start with the letter 'A'. Using Java Streams, filter the list to find all students whose names start with 'A' and print them. Then, use a stream to count how many students are in the entire list.
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Objects;
// Reusing Student class
class Student {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
public String getStudentId() { return studentId; }
public String getName() { return name; }
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return Objects.hash(studentId);
}
}
public class StudentStreamsExerciseSolution {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("S001", "Alice Smith"));
students.add(new Student("S002", "Bob Johnson"));
students.add(new Student("S003", "Anna Davis"));
students.add(new Student("S004", "Charlie Brown"));
students.add(new Student("S005", "Arthur Miller"));
students.add(new Student("S006", "Diana Prince"));
System.out.println("--- All Students ---");
students.forEach(System.out::println); // Using method reference for forEach
// 1. Filter students whose names start with 'A'
System.out.println("\n--- Students whose names start with 'A' ---");
List<Student> studentsStartingWithA = students.stream()
.filter(s -> s.getName().startsWith("A")) // Filter operation
.collect(Collectors.toList()); // Terminal operation: collect to a new List
studentsStartingWithA.forEach(System.out::println);
// 2. Count how many students are in the entire list
long totalStudents = students.stream()
.count(); // Terminal operation: count elements
System.out.println("\nTotal number of students: " + totalStudents);
}
}
Chapter 18: Wrapper Classes (Boxing/Unboxing)
Quick Theory:
Java has two main categories of data types: primitive types (like int, double, boolean, char) and reference types (objects). Primitive types store simple values directly in memory and are efficient, but they lack object-oriented features, meaning they cannot be used in contexts that expect objects, such as in the Collections Framework. For example, you cannot declare an ArrayList<int> because ArrayList can only hold objects.
To bridge this gap, Java provides Wrapper Classes for each primitive type (e.g., Integer for int, Double for double, Boolean for boolean). These wrapper classes encapsulate primitive values within objects. The process of converting a primitive to its corresponding wrapper object is called boxing, and converting a wrapper object back to its primitive value is called unboxing. Since Java 5, these conversions largely happen automatically, a feature known as autoboxing and autounboxing, which greatly simplifies code and allows primitives to seamlessly integrate with the Collections Framework and other object-oriented constructs, enhancing both reusability and flexibility.
Professional Code:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// No custom classes needed, as this chapter focuses on primitive wrappers.
// Example 1: ArrayList with Integer (Autoboxing/Autounboxing)
public class WrapperArrayListDemo {
public static void main(String[] args) {
// You cannot create an ArrayList of primitive 'int': List<int> numbers = new ArrayList<>(); (Compile error)
// Instead, use the Integer wrapper class.
List<Integer> scores = new ArrayList<>();
System.out.println("--- Adding primitive ints to ArrayList (Autoboxing) ---");
// When you add an 'int' primitive to 'scores', Java automatically converts it to an 'Integer' object.
// This is called Autoboxing.
scores.add(85); // int 85 is autoboxed to new Integer(85)
scores.add(92);
scores.add(78);
scores.add(95);
System.out.println("Scores list: " + scores);
System.out.println("\n--- Retrieving and using values (Autounboxing) ---");
// When you retrieve an 'Integer' object from 'scores' and assign it to an 'int' primitive,
// Java automatically converts the 'Integer' object back to an 'int'.
// This is called Autounboxing.
int firstScore = scores.get(0); // Integer object is autounboxed to int
System.out.println("First score (primitive): " + firstScore);
int sum = 0;
for (Integer score : scores) { // 'score' is an Integer object, but autounboxed when used in arithmetic
sum += score; // 'score' is autounboxed to int before addition
}
System.out.println("Total sum of scores: " + sum);
double average = (double) sum / scores.size();
System.out.println("Average score: " + String.format("%.2f", average));
}
}
// Example 2: HashMap with String keys and Double values
public class WrapperHashMapDemo {
public static void main(String[] args) {
// Using String as key and Double (wrapper for primitive double) as value.
Map<String, Double> productPrices = new HashMap<>();
System.out.println("--- Adding prices to Map (Autoboxing) ---");
productPrices.put("Laptop", 1200.50); // double 1200.50 autoboxed to new Double(1200.50)
productPrices.put("Keyboard", 75.25);
productPrices.put("Mouse", 20.00);
System.out.println("Product prices: " + productPrices);
System.out.println("\n--- Retrieving and calculating (Autounboxing) ---");
// Retrieving a Double object and performing arithmetic, which involves autounboxing.
double laptopPrice = productPrices.get("Laptop"); // Double object autounboxed to double primitive
System.out.println("Laptop price: $" + String.format("%.2f", laptopPrice));
double discount = 0.10; // 10% discount
// Arithmetic operations on wrapper types implicitly use autounboxing.
double discountedLaptopPrice = laptopPrice * (1 - discount);
System.out.println("Discounted Laptop price: $" + String.format("%.2f", discountedLaptopPrice));
// Adding a new entry with a calculated value
productPrices.put("Discounted Laptop", discountedLaptopPrice); // discountedLaptopPrice (primitive double) autoboxed back to Double
System.out.println("\nProduct prices after discount addition: " + productPrices);
}
}
// Example 3: Manual Boxing and Unboxing (for understanding)
public class ManualBoxingUnboxingDemo {
public static void main(String[] args) {
System.out.println("--- Manual Boxing ---");
int primitiveInt = 100;
// Manual boxing: creating a new Integer object from a primitive.
Integer boxedInt = Integer.valueOf(primitiveInt); // Preferred way (can reuse cached instances)
// Integer boxedInt_old = new Integer(primitiveInt); // Deprecated since Java 9
System.out.println("Primitive int: " + primitiveInt + ", Boxed Integer: " + boxedInt);
double primitiveDouble = 3.14;
Double boxedDouble = Double.valueOf(primitiveDouble);
System.out.println("Primitive double: " + primitiveDouble + ", Boxed Double: " + boxedDouble);
System.out.println("\n--- Manual Unboxing ---");
// Manual unboxing: extracting the primitive value from the wrapper object.
int unboxedInt = boxedInt.intValue();
System.out.println("Boxed Integer: " + boxedInt + ", Unboxed int: " + unboxedInt);
double unboxedDouble = boxedDouble.doubleValue();
System.out.println("Boxed Double: " + boxedDouble + ", Unboxed double: " + unboxedDouble);
// While manual boxing/unboxing is shown for educational purposes,
// it's generally better to let Java handle it automatically with autoboxing/autounboxing
// for cleaner and more concise code, unless specific performance or object identity
// considerations dictate otherwise.
}
}
Clean Code Tip:
Use wrapper classes (Integer, Double, Boolean, etc.) when you need to treat primitive values as objects, especially when working with Java Collections (which only store objects) or when a method requires an object type. Rely on autoboxing and autounboxing for convenience and cleaner code, as the compiler handles the conversions implicitly for you. Only perform manual boxing/unboxing if there's a specific requirement for explicit conversion.
Exercise:
Create an ArrayList<Double>. Add several primitive double values to it (e.g., 10.5, 20.3, 5.7). Iterate through the list using an enhanced for-each loop, sum up all the Double values (demonstrating autounboxing during arithmetic), and print the total sum and the average.
Solution:
import java.util.ArrayList;
import java.util.List;
public class DoubleWrapperExerciseSolution {
public static void main(String[] args) {
List<Double> measurements = new ArrayList<>();
System.out.println("--- Adding primitive doubles to ArrayList (Autoboxing) ---");
// Primitive 'double' values are automatically boxed into 'Double' objects
// when added to the List<Double>.
measurements.add(10.5);
measurements.add(20.3);
measurements.add(5.7);
measurements.add(15.0);
measurements.add(8.2);
System.out.println("Measurements list: " + measurements);
double totalSum = 0.0;
System.out.println("\n--- Summing up measurements (Autounboxing) ---");
for (Double measurement : measurements) {
// 'measurement' is a 'Double' object. When used in `totalSum += measurement`,
// it's automatically unboxed to a primitive 'double' before the addition.
totalSum += measurement;
System.out.println("Adding: " + measurement + ", Current Sum: " + String.format("%.2f", totalSum));
}
System.out.println("\nFinal total sum: " + String.format("%.2f", totalSum));
// Calculate average
if (!measurements.isEmpty()) {
double average = totalSum / measurements.size();
System.out.println("Average measurement: " + String.format("%.2f", average));
} else {
System.out.println("No measurements to calculate average.");
}
}
}
Chapter 19: Generics (<T>)
Quick Theory:
Generics are a powerful feature in Java that allows you to write classes, interfaces, and methods that operate on types specified as parameters. Instead of writing code that works with a specific type (like String or Integer), you can write code that works with a placeholder type (like T for Type), which is then replaced by an actual type at compile time. This ensures type safety by catching type-mismatch errors at compile time rather than at runtime, preventing ClassCastExceptions.
The primary benefits of generics are increased code reusability and robust type safety. A generic class like Box<T> can hold any type of object, yet the compiler ensures that you only put the correct type into it and retrieve the correct type from it, eliminating the need for explicit casting and the risk associated with it. This leads to cleaner, more readable, and significantly safer code, promoting a higher level of abstraction and scalability in your applications.
Professional Code:
// Example 1: Generic Box class
// This class can hold any type of object. 'T' is a type parameter.
class Box<T> {
private T content; // The content held by the box can be of any type T.
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public void displayContentInfo() {
System.out.println("Box content type: " + content.getClass().getName());
System.out.println("Box content value: " + content);
}
}
// Example 2: Generic method
// This method can print an array of any type.
class ArrayUtilities {
// The <E> before the return type indicates that this is a generic method.
// 'E' is a type parameter specific to this method.
public static <E> void printArray(E[] inputArray) {
System.out.print("Array elements: [");
for (int i = 0; i < inputArray.length; i++) {
System.out.print(inputArray[i]);
if (i < inputArray.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
// Another generic method example: Returning the first element of any list.
public static <T> T getFirstElement(List<T> list) {
if (list == null || list.isEmpty()) {
return null; // Or throw an exception
}
return list.get(0);
}
}
// Main method to demonstrate Generics
import java.util.ArrayList;
import java.util.List;
public class GenericsDemo {
public static void main(String[] args) {
// --- Generic Box Demo ---
System.out.println("--- Generic Box Demo ---");
// Create a Box to hold an Integer.
Box<Integer> integerBox = new Box<>(123);
integerBox.displayContentInfo();
int value = integerBox.getContent(); // No casting needed, type-safe!
System.out.println("Retrieved from integerBox: " + value);
// Create a Box to hold a String.
Box<String> stringBox = new Box<>("Hello Generics!");
stringBox.displayContentInfo();
String text = stringBox.getContent(); // No casting needed, type-safe!
System.out.println("Retrieved from stringBox: " + text);
// integerBox.setContent("Wrong type"); // Compile-time error! Type safety in action.
// --- Generic Method Demo ---
System.out.println("\n--- Generic Method Demo ---");
Integer[] intArray = {1, 2, 3, 4, 5};
ArrayUtilities.printArray(intArray); // Works with Integer array
String[] stringArray = {"Apple", "Banana", "Cherry"};
ArrayUtilities.printArray(stringArray); // Works with String array
Double[] doubleArray = {1.1, 2.2, 3.3};
ArrayUtilities.printArray(doubleArray); // Works with Double array
// Using generic method for lists
List<String> fruits = new ArrayList<>();
fruits.add("Orange");
fruits.add("Grape");
String firstFruit = ArrayUtilities.getFirstElement(fruits);
System.out.println("First fruit: " + firstFruit);
List<Integer> numbers = new ArrayList<>();
numbers.add(42);
numbers.add(99);
Integer firstNumber = ArrayUtilities.getFirstElement(numbers);
System.out.println("First number: " + firstNumber);
}
}
Clean Code Tip:
Always use generics when designing collection classes, utility methods that operate on arbitrary types, or when creating custom data structures. This enforces type safety at compile time, eliminating the need for error-prone runtime casting and making your code more readable, maintainable, and robust against ClassCastExceptions.
Exercise:
Create a generic class Pair<K, V> that can hold two values of potentially different types: a key (K) and a value (V). It should have a constructor, getters for both, and a toString() method. In your main method, create instances of Pair for:
Pair<String, Integer>(e.g., "Age", 30)Pair<Double, String>(e.g., 3.14, "PI") Print both pairs.
Solution:
// Generic class: Pair
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;
}
@Override
public String toString() {
return "Pair [Key: " + key + ", Value: " + value + "]";
}
}
public class GenericPairExerciseSolution {
public static void main(String[] args) {
System.out.println("--- Generic Pair Demo ---");
// 1. Pair of String and Integer
Pair<String, Integer> agePair = new Pair<>("Age", 30);
System.out.println(agePair);
System.out.println("Key type: " + agePair.getKey().getClass().getName());
System.out.println("Value type: " + agePair.getValue().getClass().getName());
System.out.println(); // Spacing
// 2. Pair of Double and String
Pair<Double, String> piPair = new Pair<>(3.14, "PI Constant");
System.out.println(piPair);
System.out.println("Key type: " + piPair.getKey().getClass().getName());
System.out.println("Value type: " + piPair.getValue().getClass().getName());
}
}
Chapter 20: Sorting with Comparator vs Comparable
Quick Theory:
Revisiting sorting, the choice between Comparable and Comparator is a crucial design decision for flexibility and reusability. The Comparable interface (defining a compareTo() method) allows a class to specify its "natural ordering." This is the default way objects of that class would be sorted if no other sorting logic is provided. It's an inherent property of the class, making it easy to use with Collections.sort(list).
However, what if you need to sort the same objects in multiple ways (e.g., sort Products by name, then by price, then by category)? This is where Comparator comes in. Comparators are external objects that define a comparison logic separate from the class itself. You can create multiple Comparator instances, each representing a different sorting strategy, and pass them to Collections.sort(list, comparator) or list.sort(comparator). This decouples sorting logic from the class, providing immense flexibility for diverse sorting requirements without modifying the original class.
Professional Code:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// Custom object: Product
class Product implements Comparable<Product> { // Implements Comparable for natural order
String name;
double price;
String category;
public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
// Getters for attributes
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
@Override
public String toString() {
return "Product [Name: '" + name + "', Price: $" + String.format("%.2f", price) + ", Category: '" + category + "']";
}
// --- Comparable: Defines natural ordering (by name) ---
@Override
public int compareTo(Product other) {
// Natural order: sort by product name alphabetically
return this.name.compareTo(other.name);
}
}
// Example 1: Sorting by Natural Order (Comparable)
public class ComparableSortingDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, "Electronics"));
products.add(new Product("Keyboard", 75.00, "Electronics"));
products.add(new Product("Mouse", 25.50, "Electronics"));
products.add(new Product("Coffee Maker", 99.99, "Home Appliances"));
products.add(new Product("Monitor", 300.00, "Electronics"));
System.out.println("--- Unsorted Products ---");
products.forEach(System.out::println);
// Sort using the natural order defined by Product's compareTo() (by name).
Collections.sort(products); // Or products.sort(null); since Java 8
System.out.println("\n--- Products Sorted by Name (Natural Order - Comparable) ---");
products.forEach(System.out::println);
}
}
// Example 2: Sorting by Custom Order (Comparator)
public class ComparatorSortingDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, "Electronics"));
products.add(new Product("Keyboard", 75.00, "Electronics"));
products.add(new Product("Mouse", 25.50, "Electronics"));
products.add(new Product("Coffee Maker", 99.99, "Home Appliances"));
products.add(new Product("Monitor", 300.00, "Electronics"));
System.out.println("--- Unsorted Products ---");
products.forEach(System.out::println);
// --- Custom Sorting Strategy 1: Sort by Price (Ascending) ---
// Using an anonymous inner class for Comparator (pre-Java 8 style)
Comparator<Product> priceAscComparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return Double.compare(p1.getPrice(), p2.getPrice());
}
};
Collections.sort(products, priceAscComparator);
System.out.println("\n--- Products Sorted by Price (Ascending - Comparator) ---");
products.forEach(System.out::println);
// --- Custom Sorting Strategy 2: Sort by Category (Alphabetical) then by Price (Descending) ---
// Using a Lambda Expression for Comparator (Java 8+ style) for conciseness.
// Chaining comparators for multi-level sorting (thenComparing).
Comparator<Product> categoryThenPriceDescComparator = Comparator
.comparing(Product::getCategory) // Sort by category first
.thenComparing(Comparator.comparing(Product::getPrice).reversed()); // Then by price, descending
// Can also be written as:
// Comparator<Product> categoryThenPriceDescComparator = (p1, p2) -> {
// int categoryComparison = p1.getCategory().compareTo(p2.getCategory());
// if (categoryComparison != 0) {
// return categoryComparison;
// }
// return Double.compare(p2.getPrice(), p1.getPrice()); // p2 vs p1 for descending
// };
products.sort(categoryThenPriceDescComparator); // List.sort() is also available since Java 8
System.out.println("\n--- Products Sorted by Category then Price (Descending) ---");
products.forEach(System.out::println);
}
}
Clean Code Tip:
When sorting objects, primarily define a single "natural order" using Comparable if one clearly exists. For all other sorting requirements (multiple criteria, different directions, external classes), create separate Comparators. This design promotes clean code by separating sorting logic from business logic, making your code highly flexible and reusable for various sorting scenarios.
Exercise:
Reuse your Student class from previous exercises (with studentId and name). Make Student Comparable by studentId (natural order). Then, create two Comparators: one to sort students by name alphabetically, and another to sort by studentId in reverse order. In your main method, create a list of students, then demonstrate sorting it three ways: by Comparable, by name Comparator, and by studentId reverse Comparator.
Solution:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
// Student class implements Comparable for natural ordering by studentId
class Student implements Comparable<Student> {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
public String getStudentId() { return studentId; }
public String getName() { return name; }
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
// Natural ordering: by studentId
@Override
public int compareTo(Student other) {
return this.studentId.compareTo(other.studentId);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return Objects.hash(studentId);
}
}
public class StudentSortingStrategyExerciseSolution {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("S003", "Charlie Brown"));
students.add(new Student("S001", "Alice Smith"));
students.add(new Student("S004", "David Lee"));
students.add(new Student("S002", "Bob Johnson"));
System.out.println("--- Original List ---");
students.forEach(System.out::println);
// 1. Sort by Natural Order (Comparable - by studentId)
Collections.sort(students);
System.out.println("\n--- Sorted by Student ID (Natural Order) ---");
students.forEach(System.out::println);
// 2. Sort by Name (using a Comparator)
Comparator<Student> byNameComparator = Comparator.comparing(Student::getName);
Collections.sort(students, byNameComparator);
System.out.println("\n--- Sorted by Name (Comparator) ---");
students.forEach(System.out::println);
// 3. Sort by Student ID in Reverse Order (using a Comparator)
// Using thenComparing().reversed()
Comparator<Student> byIdReverseComparator = Comparator.comparing(Student::getStudentId).reversed();
Collections.sort(students, byIdReverseComparator);
System.out.println("\n--- Sorted by Student ID (Reverse Comparator) ---");
students.forEach(System.out::println);
}
}
Chapter 21: Deep Copy vs Shallow Copy
Quick Theory: When working with objects, especially complex ones containing references to other objects, understanding deep copy and shallow copy is crucial for maintaining data integrity and avoiding unintended side effects. A shallow copy creates a new object, but instead of copying the actual values of referenced objects, it only copies their references. This means both the original and the copied object will point to the same underlying referenced objects. Modifying a referenced object through one copy will affect the other, which can be a dangerous source of bugs.
A deep copy, conversely, creates a completely independent replica of the original object, including all its nested referenced objects. It recursively copies all objects down the hierarchy, ensuring that no shared references exist between the original and the new copy. This guarantees the copied object is truly isolated from the original, promoting scalability by preventing accidental data corruption. While there's no built-in deepClone() in Java, common strategies involve implementing a "copy constructor" or using serialization techniques.
Professional Code:
// Reusable Address class (a mutable object that will be nested)
class Address {
String street;
String city;
String postalCode;
public Address(String street, String city, String postalCode) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
// Copy constructor for Address (essential for deep copy of Person)
public Address(Address other) {
this.street = other.street;
this.city = other.city;
this.postalCode = other.postalCode;
}
// Getters and Setters
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getPostalCode() { return postalCode; }
public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
@Override
public String toString() {
return "Address [Street: '" + street + "', City: '" + city + "', Postal: " + postalCode + "]";
}
}
// Person class containing an Address object
class Person {
String name;
int age;
Address homeAddress; // This is a reference to another object
public Person(String name, int age, Address homeAddress) {
this.name = name;
this.age = age;
this.homeAddress = homeAddress;
}
// --- Copy Constructor (for Deep Copy) ---
// This constructor creates a new Person object and a NEW Address object.
// It calls the copy constructor of Address to ensure the Address is also deeply copied.
public Person(Person other) {
this.name = other.name; // String is immutable, so it's effectively deep copied.
this.age = other.age; // Primitive, so it's copied by value.
// CRITICAL for deep copy: Create a NEW Address object.
this.homeAddress = new Address(other.homeAddress);
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Address getHomeAddress() { return homeAddress; }
public void setHomeAddress(Address homeAddress) { this.homeAddress = homeAddress; }
@Override
public String toString() {
return "Person [Name: '" + name + "', Age: " + age + ", Address: " + homeAddress + "]";
}
}
// Example 1: Demonstrating Shallow Copy (using assignment)
public class ShallowCopyDemo {
public static void main(String[] args) {
Address address1 = new Address("123 Main St", "Anytown", "12345");
Person person1 = new Person("Alice", 30, address1);
System.out.println("--- Original Person (person1) ---");
System.out.println("person1: " + person1);
System.out.println("person1.homeAddress hash: " + System.identityHashCode(person1.getHomeAddress()));
// --- Shallow Copy (via assignment) ---
// This does NOT create a new Person object, only a new reference pointing to the SAME object.
// It's just like objA = objB; for references.
Person person_ref = person1; // 'person_ref' now points to the same object as 'person1'.
System.out.println("\n--- Shallow Copy via Assignment (person_ref) ---");
System.out.println("person_ref: " + person_ref);
System.out.println("person_ref.homeAddress hash: " + System.identityHashCode(person_ref.getHomeAddress()));
System.out.println("Are person1 and person_ref the same object? " + (person1 == person_ref));
System.out.println("Do their addresses point to same object? " + (person1.getHomeAddress() == person_ref.getHomeAddress()));
// Modify person_ref's address. Since it's a shallow copy (same object reference),
// person1's address will also change.
System.out.println("\n--- Modifying person_ref's address ---");
person_ref.getHomeAddress().setStreet("456 Elm St");
System.out.println("person1 after person_ref modification: " + person1);
System.out.println("person_ref after person_ref modification: " + person_ref);
// Both reflect the change because they share the same Address object.
}
}
// Example 2: Demonstrating Deep Copy (using a copy constructor)
public class DeepCopyDemo {
public static void main(String[] args) {
Address originalAddress = new Address("789 Oak Ave", "Oldville", "98765");
Person originalPerson = new Person("Bob", 45, originalAddress);
System.out.println("--- Original Person (originalPerson) ---");
System.out.println("originalPerson: " + originalPerson);
System.out.println("originalPerson.homeAddress hash: " + System.identityHashCode(originalPerson.getHomeAddress()));
// --- Deep Copy (using copy constructor) ---
// Creates a new Person object AND a new Address object.
Person copiedPerson = new Person(originalPerson);
System.out.println("\n--- Deep Copied Person (copiedPerson) ---");
System.out.println("copiedPerson: " + copiedPerson);
System.out.println("copiedPerson.homeAddress hash: " + System.identityHashCode(copiedPerson.getHomeAddress()));
System.out.println("Are originalPerson and copiedPerson the same object? " + (originalPerson == copiedPerson));
System.out.println("Do their addresses point to same object? " + (originalPerson.getHomeAddress() == copiedPerson.getHomeAddress()));
// Note the different hash codes for addresses, indicating they are different objects.
// Modify copiedPerson's address. This will NOT affect originalPerson.
System.out.println("\n--- Modifying copiedPerson's address ---");
copiedPerson.getHomeAddress().setStreet("101 Pine Rd");
copiedPerson.getHomeAddress().setCity("Newville");
copiedPerson.setName("Robert"); // Modify a primitive/immutable field
System.out.println("originalPerson after copiedPerson modification: " + originalPerson);
System.out.println("copiedPerson after copiedPerson modification: " + copiedPerson);
// Original Person remains unchanged, demonstrating true independence.
}
}
Clean Code Tip:
When passing or returning objects that contain mutable (changeable) references, consider whether a deep copy is necessary. If modifying the copy should not affect the original, implement a deep copy using copy constructors. This prevents unexpected side effects, enhances scalability, and ensures type safety in terms of data integrity. Avoid Object.clone() unless you fully understand its complexities and limitations (it often performs a shallow copy by default).
Exercise:
Create a ShoppingCartItem class with productName (String) and quantity (int). Then create a ShoppingCart class that contains an ArrayList<ShoppingCartItem>. Implement a copy constructor for ShoppingCart that performs a deep copy of its items (i.e., creates new ShoppingCartItem objects for the copied list). In your main method, create an original ShoppingCart, add some items, then create a deep copy. Modify an item in the copied cart and verify that the original cart remains unchanged.
Solution:
import java.util.ArrayList;
import java.util.List;
// ShoppingCartItem class
class ShoppingCartItem {
String productName;
int quantity;
public ShoppingCartItem(String productName, int quantity) {
this.productName = productName;
this.quantity = quantity;
}
// Copy constructor for ShoppingCartItem (for deep copy)
public ShoppingCartItem(ShoppingCartItem other) {
this.productName = other.productName; // String is immutable
this.quantity = other.quantity; // Primitive
}
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
@Override
public String toString() {
return "Item [Name: '" + productName + "', Qty: " + quantity + "]";
}
}
// ShoppingCart class
class ShoppingCart {
String customerName;
List<ShoppingCartItem> items; // This is the mutable reference list
public ShoppingCart(String customerName) {
this.customerName = customerName;
this.items = new ArrayList<>(); // Initialize an empty list
}
// --- Deep Copy Constructor for ShoppingCart ---
public ShoppingCart(ShoppingCart other) {
this.customerName = other.customerName; // String is immutable
this.items = new ArrayList<>(); // CRITICAL: Create a NEW ArrayList for the copy
for (ShoppingCartItem item : other.items) {
// CRITICAL: For each item, create a NEW ShoppingCartItem object
// by calling its copy constructor. This ensures deep copy of list elements.
this.items.add(new ShoppingCartItem(item));
}
}
public void addItem(ShoppingCartItem item) {
this.items.add(item);
}
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public List<ShoppingCartItem> getItems() { return items; }
@Override
public String toString() {
return "Cart for '" + customerName + "' (Items: " + items.size() + ") -> " + items;
}
}
public class ShoppingCartDeepCopyExerciseSolution {
public static void main(String[] args) {
// Original Cart
ShoppingCart originalCart = new ShoppingCart("John Doe");
originalCart.addItem(new ShoppingCartItem("Laptop", 1));
originalCart.addItem(new ShoppingCartItem("Mouse", 2));
System.out.println("--- Original Cart ---");
System.out.println(originalCart);
System.out.println("Original cart items list hash: " + System.identityHashCode(originalCart.getItems()));
System.out.println("First item in original cart hash: " + System.identityHashCode(originalCart.getItems().get(0)));
// Create a Deep Copy
ShoppingCart copiedCart = new ShoppingCart(originalCart);
System.out.println("\n--- Deep Copied Cart ---");
System.out.println(copiedCart);
System.out.println("Copied cart items list hash: " + System.identityHashCode(copiedCart.getItems()));
System.out.println("First item in copied cart hash: " + System.identityHashCode(copiedCart.getItems().get(0)));
System.out.println("\nComparison:");
System.out.println("Are original and copied cart the same object? " + (originalCart == copiedCart)); // False
System.out.println("Do their item lists point to same object? " + (originalCart.getItems() == copiedCart.getItems())); // False
System.out.println("Do their first items point to same object? " + (originalCart.getItems().get(0) == copiedCart.getItems().get(0))); // False
// --- Modify the Copied Cart ---
System.out.println("\n--- Modifying Copied Cart ---");
copiedCart.addItem(new ShoppingCartItem("Keyboard", 1)); // Add new item
copiedCart.getItems().get(0).setQuantity(2); // Modify quantity of first item
System.out.println("Original Cart after copied cart modification: " + originalCart);
System.out.println("Copied Cart after its own modification: " + copiedCart);
// Verify that the original cart is unchanged
System.out.println("\nVerification:");
System.out.println("Original cart still has 2 items: " + (originalCart.getItems().size() == 2));
System.out.println("Original cart first item quantity is still 1: " + (originalCart.getItems().get(0).getQuantity() == 1));
}
}
Chapter 22: OOP Exception Handling
Quick Theory:
Exception handling is a critical mechanism for building robust and scalable applications. It provides a structured way to deal with unexpected or erroneous situations that occur during program execution, preventing crashes and allowing for graceful recovery or informative error reporting. Instead of relying on if-else cascades that can become unwieldy, exceptions separate the error-handling logic from the regular program flow, improving code readability and maintainability.
In Java, exceptions are objects that represent an exceptional event. You can also define your own custom exception classes by extending Exception (for checked exceptions, which must be declared with throws or handled with try-catch) or RuntimeException (for unchecked exceptions, which don't require explicit handling). Using custom exceptions enhances type safety by allowing you to create specific error types tailored to your application's domain, making error messages clearer and enabling more precise error handling by the caller. The throws keyword in a method signature declares that a method might throw a certain type of checked exception, shifting the responsibility of handling it to the caller.
Professional Code:
// Example 1: Custom Exception Class (Checked Exception)
// This exception is for when an invalid amount is provided.
class InvalidAmountException extends Exception {
public InvalidAmountException(String message) {
super(message); // Pass message to the parent Exception constructor.
}
public InvalidAmountException(String message, Throwable cause) {
super(message, cause); // Allows chaining exceptions.
}
}
// Example 2: Account class with methods that throw custom exceptions
class Account {
private String accountNumber;
private double balance;
public Account(String accountNumber, double initialBalance) throws InvalidAmountException {
if (initialBalance < 0) {
throw new InvalidAmountException("Initial balance cannot be negative.");
}
this.accountNumber = accountNumber;
this.balance = initialBalance;
System.out.println("Account " + accountNumber + " created with balance: $" + String.format("%.2f", balance));
}
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
// Method to deposit money, declares it might throw InvalidAmountException
public void deposit(double amount) throws InvalidAmountException {
if (amount <= 0) {
throw new InvalidAmountException("Deposit amount must be positive. Received: " + amount);
}
this.balance += amount;
System.out.println("Deposited $" + String.format("%.2f", amount) + ". New balance: $" + String.format("%.2f", balance));
}
// Method to withdraw money, declares it might throw InvalidAmountException
public void withdraw(double amount) throws InvalidAmountException {
if (amount <= 0) {
throw new InvalidAmountException("Withdrawal amount must be positive. Received: " + amount);
}
if (this.balance < amount) {
throw new InvalidAmountException("Insufficient funds. Current balance: $" + String.format("%.2f", balance) + ", Attempted withdrawal: $" + String.format("%.2f", amount));
}
this.balance -= amount;
System.out.println("Withdrew $" + String.format("%.2f", amount) + ". New balance: $" + String.format("%.2f", balance));
}
}
// Main method to demonstrate exception handling
public class ExceptionHandlingDemo {
public static void main(String[] args) {
Account myAccount = null; // Initialize to null
// --- Scenario 1: Handling exceptions during Account creation ---
try {
myAccount = new Account("ACC001", -100.0); // This will throw InvalidAmountException
} catch (InvalidAmountException e) {
System.err.println("Error creating account: " + e.getMessage());
}
System.out.println(); // Spacing
// Create a valid account for further operations
try {
myAccount = new Account("ACC002", 500.0);
} catch (InvalidAmountException e) {
System.err.println("This should not happen for a valid creation: " + e.getMessage());
}
// --- Scenario 2: Handling exceptions during deposit/withdrawal operations ---
if (myAccount != null) { // Ensure account was created successfully
try {
myAccount.deposit(200.0);
myAccount.withdraw(100.0);
myAccount.deposit(-50.0); // This will throw InvalidAmountException
} catch (InvalidAmountException e) {
System.err.println("Transaction error: " + e.getMessage());
}
try {
myAccount.withdraw(1000.0); // This will throw InvalidAmountException (insufficient funds)
} catch (InvalidAmountException e) {
System.err.println("Transaction error: " + e.getMessage());
}
// Catching a more general exception (RuntimeException)
// try {
// int result = 10 / 0; // ArithmeticException is a RuntimeException
// } catch (RuntimeException e) {
// System.err.println("Caught a runtime exception: " + e.getMessage());
// }
System.out.println("\nFinal balance for " + myAccount.getAccountNumber() + ": $" + String.format("%.2f", myAccount.getBalance()));
} else {
System.out.println("Account not available for transactions.");
}
}
}
Clean Code Tip:
Create custom exception classes for specific, application-domain-related errors. Extend Exception for checked exceptions (recoverable problems that callers must handle) and RuntimeException for unchecked exceptions (programming errors or unrecoverable situations). This improves type safety by making your error handling more granular and provides clearer communication about what went wrong, leading to more scalable and maintainable code.
Exercise:
Create a custom checked exception InvalidAgeException. Create a Person class with name and age. The Person constructor should throw InvalidAgeException if the age is less than 0 or greater than 150. In your main method, attempt to create Person objects with invalid ages and catch the custom exception, printing an informative error message.
Solution:
// Custom Checked Exception
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
// Person class with constructor that throws InvalidAgeException
class Person {
private String name;
private int age;
public Person(String name, int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("Age " + age + " is invalid. Age must be between 0 and 150.");
}
this.name = name;
this.age = age;
System.out.println("Person created: " + name + " (Age: " + age + ")");
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "Person [Name: '" + name + "', Age: " + age + "]";
}
}
public class InvalidAgeExceptionExerciseSolution {
public static void main(String[] args) {
System.out.println("--- Attempting to create Person objects ---");
// Valid age
try {
Person p1 = new Person("Alice", 30);
System.out.println(p1);
} catch (InvalidAgeException e) {
System.err.println("Error: " + e.getMessage());
}
System.out.println(); // Spacing
// Invalid age: negative
try {
Person p2 = new Person("Bob", -5);
System.out.println(p2);
} catch (InvalidAgeException e) {
System.err.println("Error: " + e.getMessage());
}
System.out.println(); // Spacing
// Invalid age: too high
try {
Person p3 = new Person("Charlie", 200);
System.out.println(p3);
} catch (InvalidAgeException e) {
System.err.println("Error: " + e.getMessage());
}
System.out.println(); // Spacing
// Another valid age
try {
Person p4 = new Person("Diana", 88);
System.out.println(p4);
} catch (InvalidAgeException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Chapter 23: Introduction to Lambda Expressions
Quick Theory: Lambda expressions, introduced in Java 8, are a concise way to represent anonymous functions (functions without a name). They enable functional programming paradigms in Java, drastically improving code readability and making it more scalable by reducing boilerplate code, especially when working with single-method interfaces (known as functional interfaces). Instead of creating an anonymous inner class with several lines of code to implement an interface, a lambda expression allows you to define that implementation in a single, expressive line.
Lambdas are incredibly powerful for tasks involving collections, event handling, and parallel processing. They simplify operations like filtering, mapping, and iterating over lists by allowing you to pass behavior as an argument directly. This enhances code flexibility by making it easier to compose and reuse small, focused pieces of logic. Together with the Streams API (as seen in Chapter 17), lambda expressions are a cornerstone of modern Java development, making code more declarative and easier to reason about.
Professional Code:
import java.util.ArrayList;
import java.util.Comparator; // For sorting with lambdas
import java.util.List;
import java.util.function.Consumer; // Functional interface for forEach
import java.util.function.Predicate; // Functional interface for filter, removeIf
import java.util.stream.Collectors;
// Reusing Product class from Chapter 20 (it implements Comparable)
class Product implements Comparable<Product> {
String name;
double price;
String category;
boolean inStock;
public Product(String name, double price, String category, boolean inStock) {
this.name = name;
this.price = price;
this.category = category;
this.inStock = inStock;
}
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
public boolean isInStock() { return inStock; }
@Override
public String toString() {
return "Product [Name: '" + name + "', Price: $" + String.format("%.2f", price) + ", Category: '" + category + "', In Stock: " + inStock + "]";
}
@Override
public int compareTo(Product other) {
return this.name.compareTo(other.name);
}
}
// Example 1: Basic Lambda for forEach and Comparator
public class LambdaBasicDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, "Electronics", true));
products.add(new Product("Mouse", 25.50, "Electronics", false));
products.add(new Product("Keyboard", 75.00, "Electronics", true));
products.add(new Product("Coffee Maker", 99.99, "Home Appliances", true));
products.add(new Product("Monitor", 300.00, "Electronics", false));
System.out.println("--- Products List ---");
// Using lambda for List.forEach() (Consumer functional interface)
products.forEach(p -> System.out.println("Item: " + p.getName() + " - $" + String.format("%.2f", p.getPrice())));
System.out.println("\n--- Sorting Products by Price (Lambda Comparator) ---");
// Using lambda for Collections.sort() (Comparator functional interface)
Collections.sort(products, (p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice()));
products.forEach(System.out::println);
}
}
// Example 2: Filtering with removeIf() and Streams with Lambdas
public class LambdaFilteringDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.00, "Electronics", true));
products.add(new Product("Mouse", 25.50, "Electronics", false));
products.add(new Product("Keyboard", 75.00, "Electronics", true));
products.add(new Product("Coffee Maker", 99.99, "Home Appliances", true));
products.add(new Product("Monitor", 300.00, "Electronics", false));
System.out.println("--- Original Products (Size: " + products.size() + ") ---");
products.forEach(System.out::println);
// --- Using List.removeIf() with a Lambda (Predicate functional interface) ---
// removeIf() removes all elements that satisfy the given predicate.
System.out.println("\n--- Removing Out-of-Stock Products with removeIf() ---");
boolean changed = products.removeIf(p -> !p.isInStock()); // Remove if not in stock
System.out.println("List changed: " + changed);
System.out.println("Products after removeIf (Size: " + products.size() + "): ");
products.forEach(System.out::println);
// Reset products for next demo
products.clear();
products.add(new Product("Laptop", 1200.00, "Electronics", true));
products.add(new Product("Mouse", 25.50, "Electronics", false));
products.add(new Product("Keyboard", 75.00, "Electronics", true));
products.add(new Product("Coffee Maker", 99.99, "Home Appliances", true));
products.add(new Product("Monitor", 300.00, "Electronics", false));
// --- Filtering with Streams and Lambdas ---
System.out.println("\n--- Filtering Products by Category and Price with Streams ---");
// Filter products that are "Electronics" AND cost more than $100.
List<Product> expensiveElectronics = products.stream()
.filter(p -> p.getCategory().equals("Electronics")) // First filter
.filter(p -> p.getPrice() > 100.00) // Second filter
.collect(Collectors.toList()); // Collect results
System.out.println("Expensive Electronics (Stream result):");
expensiveElectronics.forEach(System.out::println);
}
}
Clean Code Tip:
Embrace lambda expressions for implementing functional interfaces, especially in contexts like iterating with forEach, filtering with removeIf or Streams, and defining custom sorting logic with Comparator. This makes your code more concise, expressive, and easier to understand, reflecting modern Java practices for scalability and flexibility.
Exercise:
Reuse your Student class (studentId, name). Create an ArrayList<Student>. Add at least 5 students.
- Use
List.forEach()with a lambda to print all student names. - Use
List.removeIf()with a lambda to remove all students whosestudentIdends with an odd number (e.g., "S001", "S003", "S005"). - Print the list again to show the removed students.
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// Reusing Student class
class Student {
String studentId;
String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
public String getStudentId() { return studentId; }
public String getName() { return name; }
@Override
public String toString() {
return "Student [ID: " + studentId + ", Name: " + name + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return studentId.equals(student.studentId);
}
@Override
public int hashCode() {
return Objects.hash(studentId);
}
}
public class LambdaExerciseSolution {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("S001", "Alice Smith"));
students.add(new Student("S002", "Bob Johnson"));
students.add(new Student("S003", "Anna Davis"));
students.add(new Student("S004", "Charlie Brown"));
students.add(new Student("S005", "Arthur Miller"));
students.add(new Student("S006", "Diana Prince"));
System.out.println("--- Initial Student List ---");
// 1. Print all student names using forEach with a lambda
students.forEach(s -> System.out.println("Name: " + s.getName() + " (ID: " + s.getStudentId() + ")"));
// 2. Remove students whose studentId ends with an odd number
System.out.println("\n--- Removing students with odd-ending IDs ---");
// The condition `Integer.parseInt(s.getStudentId().substring(s.getStudentId().length() - 1)) % 2 != 0`
// gets the last digit of the studentId, converts it to an int, and checks if it's odd.
students.removeIf(s -> {
String lastDigitStr = s.getStudentId().substring(s.getStudentId().length() - 1);
int lastDigit = Integer.parseInt(lastDigitStr);
return lastDigit % 2 != 0;
});
System.out.println("\n--- Student List After Removal (Students with even-ending IDs) ---");
// 3. Print the list again to show the removed students
students.forEach(System.out::println);
}
}
Chapter 24: Final Project Logic (Mini System)
Quick Theory: This final chapter synthesizes many OOP concepts into a small, cohesive system. The goal is to demonstrate how inheritance, polymorphism, the Collections Framework, and sorting work together to create a flexible and scalable application. Building such a system from the ground up helps solidify understanding of how individual components contribute to a larger architecture, ensuring type safety and clean design.
A well-designed system, even a small one, typically follows these principles: define a common base (abstract class or interface) for related entities, use specialized subclasses to implement concrete behaviors, store these entities in collections for easy management, and leverage polymorphism to process them uniformly. Sorting mechanisms enhance the user experience by presenting data in a logical order, while exceptions ensure robustness. This structured approach, a common pattern in DAM final projects, emphasizes reusability and maintainability, providing a solid foundation for more complex applications.
Professional Code:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional; // For safer retrieval from streams
import java.util.stream.Collectors;
// --- 1. Custom Exception for System specific errors ---
class SystemException extends Exception {
public SystemException(String message) {
super(message);
}
}
// --- 2. Base Class (Abstract) for Assets (Inheritance & Polymorphism) ---
abstract class Asset implements Comparable<Asset> { // Asset implements Comparable for natural order
private String assetId;
private String name;
private double acquisitionCost;
public Asset(String assetId, String name, double acquisitionCost) throws SystemException {
if (assetId == null || assetId.trim().isEmpty()) {
throw new SystemException("Asset ID cannot be empty.");
}
if (name == null || name.trim().isEmpty()) {
throw new SystemException("Asset name cannot be empty.");
}
if (acquisitionCost <= 0) {
throw new SystemException("Acquisition cost must be positive.");
}
this.assetId = assetId;
this.name = name;
this.acquisitionCost = acquisitionCost;
}
// Getters for common attributes
public String getAssetId() { return assetId; }
public String getName() { return name; }
public double getAcquisitionCost() { return acquisitionCost; }
// Abstract method: forces subclasses to define how they calculate depreciation
public abstract double calculateDepreciation(int years);
// Concrete method: displays basic asset info
public void displayBasicInfo() {
System.out.println("Asset ID: " + assetId + ", Name: " + name + ", Cost: $" + String.format("%.2f", acquisitionCost));
}
// Natural order for Asset: by assetId
@Override
public int compareTo(Asset other) {
return this.assetId.compareTo(other.assetId);
}
// Crucial for Set/Map keys based on AssetId uniqueness
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Asset asset = (Asset) o;
return Objects.equals(assetId, asset.assetId);
}
@Override
public int hashCode() {
return Objects.hash(assetId);
}
}
// --- 3. Concrete Subclasses (Specialization) ---
class Computer extends Asset {
private String processor;
private int ramGB;
public Computer(String assetId, String name, double acquisitionCost, String processor, int ramGB) throws SystemException {
super(assetId, name, acquisitionCost);
if (processor == null || processor.trim().isEmpty()) {
throw new SystemException("Processor cannot be empty.");
}
if (ramGB <= 0) {
throw new SystemException("RAM must be positive.");
}
this.processor = processor;
this.ramGB = ramGB;
}
@Override
public double calculateDepreciation(int years) {
// Simple linear depreciation for computers: 20% per year for up to 5 years.
if (years <= 0) return 0;
double depreciationRate = 0.20;
double totalDepreciation = getAcquisitionCost() * Math.min(years, 5) * depreciationRate;
return Math.min(totalDepreciation, getAcquisitionCost()); // Cannot depreciate more than cost.
}
@Override
public String toString() {
return String.format("Computer [ID: %s, Name: %s, Cost: $%.2f, Proc: %s, RAM: %dGB]",
getAssetId(), getName(), getAcquisitionCost(), processor, ramGB);
}
}
class Vehicle extends Asset {
private String make;
private int year;
public Vehicle(String assetId, String name, double acquisitionCost, String make, int year) throws SystemException {
super(assetId, name, acquisitionCost);
if (make == null || make.trim().isEmpty()) {
throw new SystemException("Vehicle make cannot be empty.");
}
if (year <= 1900 || year > java.time.Year.now().getValue()) {
throw new SystemException("Invalid vehicle year.");
}
this.make = make;
this.year = year;
}
@Override
public double calculateDepreciation(int years) {
// Simple linear depreciation for vehicles: 15% per year for up to 7 years.
if (years <= 0) return 0;
double depreciationRate = 0.15;
double totalDepreciation = getAcquisitionCost() * Math.min(years, 7) * depreciationRate;
return Math.min(totalDepreciation, getAcquisitionCost());
}
@Override
public String toString() {
return String.format("Vehicle [ID: %s, Name: %s, Cost: $%.2f, Make: %s, Year: %d]",
getAssetId(), getName(), getAcquisitionCost(), make, year);
}
}
// --- 4. Asset Management System (Collections, Sorting, Lambdas) ---
class AssetManagementSystem {
private List<Asset> assets; // Use List to store assets (Polymorphism)
public AssetManagementSystem() {
this.assets = new ArrayList<>();
}
public void addAsset(Asset asset) throws SystemException {
// Check for duplicate Asset ID before adding
if (assets.stream().anyMatch(a -> a.getAssetId().equals(asset.getAssetId()))) {
throw new SystemException("Asset with ID " + asset.getAssetId() + " already exists.");
}
this.assets.add(asset);
System.out.println("Added: " + asset.getName() + " (" + asset.getAssetId() + ")");
}
public void removeAsset(String assetId) throws SystemException {
boolean removed = assets.removeIf(a -> a.getAssetId().equals(assetId)); // Lambda for removeIf
if (!removed) {
throw new SystemException("Asset with ID " + assetId + " not found for removal.");
}
System.out.println("Removed asset with ID: " + assetId);
}
public Optional<Asset> findAssetById(String assetId) {
// Stream API for efficient search
return assets.stream()
.filter(a -> a.getAssetId().equals(assetId))
.findFirst(); // Returns an Optional, preventing NullPointerExceptions
}
public List<Asset> getAllAssets() {
return Collections.unmodifiableList(assets); // Return an unmodifiable list for safety
}
public void displayAllAssets() {
if (assets.isEmpty()) {
System.out.println("No assets in the system.");
return;
}
System.out.println("\n--- Current Assets (Sorted by ID) ---");
// Sort by natural order (AssetId)
Collections.sort(assets);
assets.forEach(System.out::println); // Lambda for forEach
}
public void displayAssetsSortedByCost(boolean ascending) {
if (assets.isEmpty()) {
System.out.println("No assets in the system.");
return;
}
System.out.println("\n--- Current Assets (Sorted by Acquisition Cost " + (ascending ? "Asc" : "Desc") + ") ---");
// Custom sorting using Comparator (Lambda)
Comparator<Asset> costComparator = Comparator.comparing(Asset::getAcquisitionCost);
if (!ascending) {
costComparator = costComparator.reversed();
}
assets.sort(costComparator); // List.sort() with Comparator
assets.forEach(System.out::println);
}
public void displayDepreciationReport(int years) {
if (assets.isEmpty()) {
System.out.println("No assets to report depreciation.");
return;
}
System.out.println(String.format("\n--- Depreciation Report (after %d years) ---", years));
assets.forEach(asset -> {
double currentDepreciation = asset.calculateDepreciation(years); // Polymorphic call
System.out.println(String.format("Asset ID: %s, Name: %s, Cost: $%.2f, Depreciation: $%.2f",
asset.getAssetId(), asset.getName(), asset.getAcquisitionCost(), currentDepreciation));
});
}
}
// --- 5. Main Application Logic ---
public class FinalProjectSystemDemo {
public static void main(String[] args) {
AssetManagementSystem system = new AssetManagementSystem();
// Adding Assets
try {
system.addAsset(new Computer("COMP001", "Desktop Workstation", 1800.00, "Intel i7", 16));
system.addAsset(new Vehicle("VEH001", "Company Car A", 25000.00, "Toyota", 2020));
system.addAsset(new Computer("COMP002", "Laptop Pro", 1100.00, "Ryzen 5", 8));
system.addAsset(new Vehicle("VEH002", "Delivery Van", 35000.00, "Ford", 2022));
// system.addAsset(new Computer("COMP001", "Duplicate ID", 500.00, "Intel i3", 4)); // Will throw SystemException
} catch (SystemException e) {
System.err.println("Error adding asset: " + e.getMessage());
} catch (Exception e) { // Catch any other unexpected exceptions
System.err.println("An unexpected error occurred: " + e.getMessage());
}
system.displayAllAssets(); // Sorted by Asset ID
system.displayAssetsSortedByCost(true); // Sorted by Cost Ascending
system.displayAssetsSortedByCost(false); // Sorted by Cost Descending
// Finding an Asset
String searchId = "VEH001";
Optional<Asset> foundAsset = system.findAssetById(searchId);
if (foundAsset.isPresent()) {
System.out.println("\nFound Asset " + searchId + ": " + foundAsset.get().getName());
// Downcast safely with instanceof for specific actions if needed
if (foundAsset.get() instanceof Vehicle vehicle) {
System.out.println("This is a " + vehicle.getMake() + " from " + vehicle.getYear());
}
} else {
System.out.println("\nAsset with ID " + searchId + " not found.");
}
// Depreciation Report (Polymorphism in action)
system.displayDepreciationReport(3);
// Removing an Asset
try {
system.removeAsset("COMP002");
system.removeAsset("NONEXISTENT"); // Will throw SystemException
} catch (SystemException e) {
System.err.println("Error removing asset: " + e.getMessage());
}
system.displayAllAssets(); // Final list
}
}
Clean Code Tip:
Design your system with clear abstractions (abstract classes, interfaces) and leverage polymorphism to process diverse objects uniformly. Use collections (like List) to manage groups of objects, and employ Comparators and Lambdas for flexible sorting and filtering. Implement custom exceptions for domain-specific error handling. This holistic approach ensures your code is scalable, reusable, and type-safe, forming a robust foundation for any project.
3º Java - The Java Engineer: Data, Testing & Tooling:
Chapter 1: Lambda Expressions Deep Dive
Technical Theory: Imperative vs Declarative
In software development, we often distinguish between two primary programming styles: imperative and declarative.
- Imperative Programming: Focuses on how to achieve a result. You provide explicit, step-by-step instructions that directly change the program's state. Think of it like giving a robot precise commands for each movement.
- Declarative Programming: Focuses on what result you want, without necessarily detailing the exact steps. You describe the desired outcome, and the system figures out the how. Think of it like ordering a coffee – you state "I want a latte," and the barista handles the process.
Lambda expressions in Java lean heavily towards the declarative style. They allow us to treat functionality as a method argument or code as data, making our code more expressive and concise, especially when working with functional interfaces.
The syntax of a lambda expression is (parameters) -> {body}.
parameters: The input parameters, similar to method parameters.->: The lambda arrow operator, separating parameters from the body.body: The logic to be executed. This can be a single expression (which is implicitly returned) or a block of statements.
Functional interfaces are crucial here; they are interfaces with exactly one abstract method. Lambdas provide an inline implementation for these single-method interfaces. Common built-in functional interfaces include Predicate<T> (takes T, returns boolean), and Consumer<T> (takes T, returns void).
Professional Code
Let's see how lambdas replace verbose anonymous inner classes.
Example 1: Implementing Runnable
// Before: Anonymous Inner Class
class TaskRunnerBefore {
public void execute() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Executing task (old way)");
}
});
thread.start();
}
}
// After: Lambda Expression
class TaskRunnerAfter {
public void execute() {
Thread thread = new Thread(() -> System.out.println("Executing task (new way)"));
thread.start();
}
}
public class LambdaRunnableExample {
public static void main(String[] args) {
new TaskRunnerBefore().execute();
new TaskRunnerAfter().execute();
}
}
Example 2: Filtering a list with Predicate
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
// Before: Custom filtering logic
class UserFilterBefore {
public List<User> filterUsersByAge(List<User> users, int minAge) {
List<User> filteredUsers = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= minAge) {
filteredUsers.add(user);
}
}
return filteredUsers;
}
}
// After: Using Predicate with Lambda
class UserFilterAfter {
public List<User> filterUsers(List<User> users, Predicate<User> predicate) {
List<User> filteredUsers = new ArrayList<>();
for (User user : users) {
if (predicate.test(user)) {
filteredUsers.add(user);
}
}
return filteredUsers;
}
}
public class PredicateLambdaExample {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new User("Alice", 25));
users.add(new User("Bob", 30));
users.add(new User("Charlie", 20));
System.out.println("--- Filtering Before ---");
UserFilterBefore filterBefore = new UserFilterBefore();
List<User> oldUsersBefore = filterBefore.filterUsersByAge(users, 28);
oldUsersBefore.forEach(System.out::println);
System.out.println("--- Filtering After ---");
UserFilterAfter filterAfter = new UserFilterAfter();
// Lambda for users older than 28
List<User> oldUsersAfter = filterAfter.filterUsers(users, user -> user.getAge() > 28);
oldUsersAfter.forEach(System.out::println);
// Another lambda for users whose name starts with 'A'
List<User> usersWithNameA = filterAfter.filterUsers(users, user -> user.getName().startsWith("A"));
System.out.println("\nUsers with name starting with 'A':");
usersWithNameA.forEach(System.out::println);
}
}
Example 3: Processing elements with Consumer
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
// Before: Traditional loop for processing
class ListProcessorBefore {
public void process(List<String> items) {
for (String item : items) {
System.out.println("Processing item (old way): " + item);
}
}
}
// After: Using Consumer with Lambda
class ListProcessorAfter {
public void process(List<String> items, Consumer<String> consumer) {
for (String item : items) {
consumer.accept(item);
}
}
}
public class ConsumerLambdaExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Java", "Kotlin", "Scala");
System.out.println("--- Processing Before ---");
ListProcessorBefore processorBefore = new ListProcessorBefore();
processorBefore.process(names);
System.out.println("--- Processing After ---");
ListProcessorAfter processorAfter = new ListProcessorAfter();
// Lambda for printing each item
processorAfter.process(names, item -> System.out.println("Processing item (new way): " + item.toUpperCase()));
// Another lambda for custom action
System.out.println("\n--- Custom Processing ---");
processorAfter.process(names, item -> {
System.out.println("Item length: " + item.length());
});
}
}
Clean Code Tip: Less code = Fewer bugs Lambda expressions significantly reduce boilerplate code, especially when dealing with functional interfaces. Less code means less surface area for bugs to hide, and it's generally easier to read and maintain concise, focused pieces of logic.
Exercise & Solution
Exercise: Given a list of String objects, filter out all strings that have a length less than 5 characters using a Predicate lambda. Then, print the filtered strings.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class LambdaExercise {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("apple");
words.add("cat");
words.add("banana");
words.add("dog");
words.add("elephant");
words.add("go");
System.out.println("Original words: " + words);
// Your code here: Filter words with length < 5 using a Predicate lambda
// And store them in a new list called 'longWords'
List<String> longWords = new ArrayList<>();
// ...
// Solution placeholder
// ...
System.out.println("Words with length >= 5: " + longWords);
}
}
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors; // Will be covered in Chapter 3, but useful here
public class LambdaExerciseSolution {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("apple");
words.add("cat");
words.add("banana");
words.add("dog");
words.add("elephant");
words.add("go");
System.out.println("Original words: " + words);
// Solution using Predicate and a loop
List<String> longWords = new ArrayList<>();
Predicate<String> isLongEnough = word -> word.length() >= 5;
for (String word : words) {
if (isLongEnough.test(word)) {
longWords.add(word);
}
}
// Alternative (more modern) solution using Streams (covered in next chapters)
// List<String> longWords = words.stream()
// .filter(word -> word.length() >= 5)
// .collect(Collectors.toList());
System.out.println("Words with length >= 5: " + longWords);
}
}
Chapter 2: The Stream API (Filter & Map)
Technical Theory: How to process collections like a pro
The Java Stream API, introduced in Java 8, provides a powerful and declarative way to process collections of data. A stream is a sequence of elements that supports sequential and parallel aggregate operations. Crucially, streams are:
- Not a data structure: They don't store data themselves. They act as a pipeline for processing data from a source (like a
List,Set,array, orI/O channel). - Functional in nature: Operations on streams produce a new stream without modifying the underlying data source (non-mutating).
- Lazy: Intermediate operations (like
filter,map) are not executed until a terminal operation (likecollect,forEach) is invoked.
filter() and map() are two of the most fundamental intermediate operations in the Stream API.
.filter(Predicate<T> predicate): Takes aPredicate(a lambda that returns a boolean) and returns a new stream containing only the elements that satisfy the predicate. It's like sifting through a collection, keeping only what matches your criteria..map(Function<T, R> mapper): Takes aFunction(a lambda that transforms an element of typeTto typeR) and returns a new stream where each element has been transformed. It's like taking a list of items and converting each one into something else (e.g., users to names, numbers to squares).
Professional Code
We'll use our User class from the previous chapter.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class User {
private String name;
private int age;
private String email;
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + ", email='" + email + '\'' + '}';
}
}
public class StreamFilterMapExample {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new User("Alice", 25, "alice@example.com"));
users.add(new User("Bob", 30, "bob@example.com"));
users.add(new User("Charlie", 20, "charlie@example.com"));
users.add(new User("David", 35, "david@example.com"));
users.add(new User("Eve", 28, "eve@example.com"));
// Example 1: Filtering a list of 'Users' by age
System.out.println("--- Filtering Users by Age ---");
// Before: Imperative style
List<User> youngUsersBefore = new ArrayList<>();
for (User user : users) {
if (user.getAge() < 30) {
youngUsersBefore.add(user);
}
}
System.out.println("Before (age < 30): " + youngUsersBefore);
// After: Stream API with filter()
List<User> youngUsersAfter = users.stream()
.filter(user -> user.getAge() < 30)
.collect(Collectors.toList());
System.out.println("After (age < 30): " + youngUsersAfter);
// Example 2: Transforming a list of 'Users' into a list of 'Names'
System.out.println("\n--- Transforming Users to Names ---");
// Before: Imperative style
List<String> userNamesBefore = new ArrayList<>();
for (User user : users) {
userNamesBefore.add(user.getName());
}
System.out.println("Before (names): " + userNamesBefore);
// After: Stream API with map()
List<String> userNamesAfter = users.stream()
.map(User::getName) // Method reference, covered in Chapter 6, but common here. Equivalent to user -> user.getName()
.collect(Collectors.toList());
System.out.println("After (names): " + userNamesAfter);
// Example 3: Chaining Filter and Map - Filter by age, then get names
System.out.println("\n--- Filter by Age AND Get Names ---");
// Before: Imperative style (multiple loops or nested conditions)
List<String> namesOfAdultUsersBefore = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 25) { // Filter
namesOfAdultUsersBefore.add(user.getName()); // Map
}
}
System.out.println("Before (age >= 25 names): " + namesOfAdultUsersBefore);
// After: Stream API with chained filter() and map()
List<String> namesOfAdultUsersAfter = users.stream()
.filter(user -> user.getAge() >= 25) // Intermediate operation
.map(User::getName) // Intermediate operation
.collect(Collectors.toList()); // Terminal operation
System.out.println("After (age >= 25 names): " + namesOfAdultUsersAfter);
}
}
Clean Code Tip: Stream operations are declarative and compose well
Instead of writing explicit loops and conditional logic (imperative), streams allow you to declare what you want to achieve (filter, map, sort). This makes your code much more readable, especially when chaining multiple operations. Each operation focuses on a single responsibility, leading to cleaner, more maintainable code.
Exercise & Solution
Exercise: Given a list of Product objects, filter for products that are inStock and have a price greater than 50. Then, transform these filtered products into a list of Strings, where each string combines the product's name and price (e.g., "Laptop ($1200.0)").
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class Product {
private String name;
private double price;
private boolean inStock;
public Product(String name, double price, boolean inStock) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public boolean isInStock() {
return inStock;
}
@Override
public String toString() {
return "Product{" + "name='" + name + '\'' + ", price=" + price + ", inStock=" + inStock + '}';
}
}
public class StreamFilterMapExercise {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Keyboard", 75.0, true));
products.add(new Product("Mouse", 25.0, true));
products.add(new Product("Monitor", 150.0, false));
products.add(new Product("Laptop", 1200.0, true));
products.add(new Product("Webcam", 40.0, true));
products.add(new Product("Headphones", 90.0, false));
System.out.println("Original products: " + products);
// Your code here: Filter in-stock products with price > 50,
// then map to "Name ($Price)" strings.
List<String> highValueInStockProducts = new ArrayList<>();
// ...
// Solution placeholder
// ...
System.out.println("High-value in-stock products (name and price): " + highValueInStockProducts);
}
}
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class Product {
private String name;
private double price;
private boolean inStock;
public Product(String name, double price, boolean inStock) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public boolean isInStock() {
return inStock;
}
@Override
public String toString() {
return "Product{" + "name='" + name + '\'' + ", price=" + price + ", inStock=" + inStock + '}';
}
}
public class StreamFilterMapExerciseSolution {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Keyboard", 75.0, true));
products.add(new Product("Mouse", 25.0, true));
products.add(new Product("Monitor", 150.0, false));
products.add(new Product("Laptop", 1200.0, true));
products.add(new Product("Webcam", 40.0, true));
products.add(new Product("Headphones", 90.0, false));
System.out.println("Original products: " + products);
List<String> highValueInStockProducts = products.stream()
.filter(product -> product.isInStock() && product.getPrice() > 50.0)
.map(product -> product.getName() + " ($" + product.getPrice() + ")")
.collect(Collectors.toList());
System.out.println("High-value in-stock products (name and price): " + highValueInStockProducts);
}
}
Chapter 3: Stream Terminal Operations
Technical Theory: Triggering the Stream Pipeline
Intermediate stream operations (like filter, map, sorted) are lazy. They return a new stream and don't perform any actual computation until a terminal operation is invoked. A terminal operation consumes the stream and produces a result or a side effect. After a terminal operation, the stream cannot be reused.
Let's explore some common terminal operations:
.collect(Collector<T, A, R> collector): This is one of the most powerful terminal operations. It takes aCollectoras an argument, which defines how the elements in the stream should be accumulated into a final result.Collectorsis a utility class providing many predefined collectors, such asCollectors.toList(),Collectors.toSet(),Collectors.joining(),Collectors.groupingBy(), etc..count(): Returns the number of elements in the stream as along..forEach(Consumer<T> action): Performs an action for each element in the stream. It's often used for side effects, like printing elements to the console.
Professional Code
We'll continue using our User class.
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class User {
private String name;
private int age;
private String city;
public User(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getCity() {
return city;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + '}';
}
}
public class StreamTerminalOperationsExample {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new User("Alice", 25, "New York"));
users.add(new User("Bob", 30, "London"));
users.add(new User("Charlie", 20, "New York"));
users.add(new User("David", 35, "Paris"));
users.add(new User("Eve", 28, "London"));
users.add(new User("Frank", 40, "New York"));
System.out.println("Original Users: " + users);
// Example 1: Collecting results into a List
System.out.println("\n--- Collect to List ---");
// Before: Manual iteration and adding
List<String> namesOfAdultsBefore = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 25) {
namesOfAdultsBefore.add(user.getName());
}
}
System.out.println("Adult names (Before): " + namesOfAdultsBefore);
// After: Using .filter().map().collect(Collectors.toList())
List<String> namesOfAdultsAfter = users.stream()
.filter(user -> user.getAge() >= 25)
.map(User::getName)
.collect(Collectors.toList());
System.out.println("Adult names (After): " + namesOfAdultsAfter);
// Example 2: Counting elements
System.out.println("\n--- Counting Elements ---");
// Before: Manual counter
int nyUsersCountBefore = 0;
for (User user : users) {
if (user.getCity().equals("New York")) {
nyUsersCountBefore++;
}
}
System.out.println("Users in New York (Before): " + nyUsersCountBefore);
// After: Using .filter().count()
long nyUsersCountAfter = users.stream()
.filter(user -> user.getCity().equals("New York"))
.count();
System.out.println("Users in New York (After): " + nyUsersCountAfter);
// Example 3: Performing an action for each element
System.out.println("\n--- For Each Element ---");
// Before: Enhanced for loop
System.out.println("All users (Before):");
for (User user : users) {
System.out.println("- " + user.getName() + " is " + user.getAge());
}
// After: Using .forEach()
System.out.println("All users (After):");
users.stream()
.forEach(user -> System.out.println("- " + user.getName() + " is " + user.getAge()));
// Bonus: Grouping users by city using Collectors.groupingBy
System.out.println("\n--- Grouping Users by City ---");
Map<String, List<User>> usersByCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
usersByCity.forEach((city, userList) -> {
System.out.println("City: " + city);
userList.forEach(user -> System.out.println(" - " + user.getName()));
});
}
}
Clean Code Tip: Use appropriate terminal operations for specific needs
Don't collect to a list if all you need is a count. Don't forEach if you need to build a new collection. Choosing the right terminal operation makes your intent clear and often leads to more efficient code by avoiding unnecessary intermediate data structures.
Exercise & Solution
Exercise: You have a list of Order objects.
- Count how many orders have a
statusof "PENDING". - Collect the
customerNameof all orders that have astatusof "COMPLETED" into aList<String>.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class Order {
private String orderId;
private String customerName;
private String status;
private double totalAmount;
public Order(String orderId, String customerName, String status, double totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.status = status;
this.totalAmount = totalAmount;
}
public String getOrderId() {
return orderId;
}
public String getCustomerName() {
return customerName;
}
public String getStatus() {
return status;
}
public double getTotalAmount() {
return totalAmount;
}
@Override
public String toString() {
return "Order{" + "orderId='" + orderId + '\'' + ", customerName='" + customerName + '\'' + ", status='" + status + '\'' + ", totalAmount=" + totalAmount + '}';
}
}
public class StreamTerminalExercise {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>();
orders.add(new Order("ORD001", "Alice", "PENDING", 150.0));
orders.add(new Order("ORD002", "Bob", "COMPLETED", 200.0));
orders.add(new Order("ORD003", "Charlie", "PENDING", 75.0));
orders.add(new Order("ORD004", "David", "COMPLETED", 300.0));
orders.add(new Order("ORD005", "Eve", "PROCESSING", 100.0));
orders.add(new Order("ORD006", "Frank", "COMPLETED", 50.0));
System.out.println("All Orders: " + orders);
// Your code here:
// 1. Count pending orders
long pendingOrdersCount = 0;
// 2. Collect customer names of completed orders
List<String> completedOrderCustomerNames = new ArrayList<>();
// ...
// Solution placeholder
// ...
System.out.println("\nNumber of PENDING orders: " + pendingOrdersCount);
System.out.println("Customer names for COMPLETED orders: " + completedOrderCustomerNames);
}
}
Solution:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class Order {
private String orderId;
private String customerName;
private String status;
private double totalAmount;
public Order(String orderId, String customerName, String status, double totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.status = status;
this.totalAmount = totalAmount;
}
public String getOrderId() {
return orderId;
}
public String getCustomerName() {
return customerName;
}
public String getStatus() {
return status;
}
public double getTotalAmount() {
return totalAmount;
}
@Override
public String toString() {
return "Order{" + "orderId='" + orderId + '\'' + ", customerName='" + customerName + '\'' + ", status='" + status + '\'' + ", totalAmount=" + totalAmount + '}';
}
}
public class StreamTerminalExerciseSolution {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>();
orders.add(new Order("ORD001", "Alice", "PENDING", 150.0));
orders.add(new Order("ORD002", "Bob", "COMPLETED", 200.0));
orders.add(new Order("ORD003", "Charlie", "PENDING", 75.0));
orders.add(new Order("ORD004", "David", "COMPLETED", 300.0));
orders.add(new Order("ORD005", "Eve", "PROCESSING", 100.0));
orders.add(new Order("ORD006", "Frank", "COMPLETED", 50.0));
System.out.println("All Orders: " + orders);
// 1. Count pending orders
long pendingOrdersCount = orders.stream()
.filter(order -> "PENDING".equals(order.getStatus()))
.count();
// 2. Collect customer names of completed orders
List<String> completedOrderCustomerNames = orders.stream()
.filter(order -> "COMPLETED".equals(order.getStatus()))
.map(Order::getCustomerName)
.collect(Collectors.toList());
System.out.println("\nNumber of PENDING orders: " + pendingOrdersCount);
System.out.println("Customer names for COMPLETED orders: " + completedOrderCustomerNames);
}
}
Chapter 4: Sorting with Streams
Technical Theory: Sorting Made Easy
The sorted() intermediate operation in the Stream API allows you to sort elements within a stream. It comes in two main forms:
.sorted(): Sorts elements according to their natural order. This requires the elements to implement theComparableinterface. For example,Stringand wrapper classes likeIntegeralready implementComparable..sorted(Comparator<T> comparator): Sorts elements according to the order induced by the providedComparator. This is far more common for custom objects, where you define your own sorting logic using a lambda expression or method reference.
The Comparator interface is a functional interface, making it a perfect candidate for lambdas. The Comparator.comparing() static method is incredibly useful for creating comparators based on extracting a comparable key. You can also chain comparators using thenComparing().
Professional Code
We'll use a slightly modified User class to demonstrate sorting.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class User implements Comparable<User> { // Implementing Comparable for natural order by name
private String name;
private int age;
private String department;
public User(String name, int age, String department) {
this.name = name;
this.age = age;
this.department = department;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getDepartment() {
return department;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + ", department='" + department + '\'' + '}';
}
// Natural order by name (for .sorted() without a comparator)
@Override
public int compareTo(User other) {
return this.name.compareTo(other.name);
}
}
public class StreamSortingExample {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new User("Alice", 30, "HR"));
users.add(new User("Charlie", 25, "Engineering"));
users.add(new User("Bob", 35, "HR"));
users.add(new User("David", 22, "Marketing"));
users.add(new User("Eve", 30, "Engineering"));
System.out.println("Original Users:");
users.forEach(System.out::println);
// Example 1: Sorting a list of objects by natural order (if Comparable)
System.out.println("\n--- Sorting by Natural Order (User Name) ---");
// Before: Collections.sort with custom object
List<User> sortedUsersNaturalBefore = new ArrayList<>(users);
sortedUsersNaturalBefore.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u1.getName().compareTo(u2.getName());
}
});
System.out.println("Before (by Name):");
sortedUsersNaturalBefore.forEach(System.out::println);
// After: Stream API with .sorted() (relies on User implementing Comparable<User>)
List<User> sortedUsersNaturalAfter = users.stream()
.sorted() // Uses User's compareTo method
.collect(Collectors.toList());
System.out.println("After (by Name):");
sortedUsersNaturalAfter.forEach(System.out::println);
// Example 2: Sorting User objects by age (custom comparator)
System.out.println("\n--- Sorting Users by Age Ascending ---");
// Before: Anonymous Comparator
List<User> sortedByAgeBefore = new ArrayList<>(users);
sortedByAgeBefore.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return Integer.compare(u1.getAge(), u2.getAge());
}
});
System.out.println("Before (by Age):");
sortedByAgeBefore.forEach(System.out::println);
// After: Stream API with .sorted(Comparator.comparingInt)
List<User> sortedByAgeAfter = users.stream()
.sorted(Comparator.comparingInt(User::getAge))
.collect(Collectors.toList());
System.out.println("After (by Age):");
sortedByAgeAfter.forEach(System.out::println);
// Example 3: Sorting User objects by department then by age (chained comparators)
System.out.println("\n--- Sorting by Department then Age Descending ---");
// Before: Chained comparators (verbose)
List<User> sortedByDeptAgeBefore = new ArrayList<>(users);
sortedByDeptAgeBefore.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
int deptCompare = u1.getDepartment().compareTo(u2.getDepartment());
if (deptCompare != 0) {
return deptCompare;
}
// If departments are same, sort by age descending
return Integer.compare(u2.getAge(), u1.getAge()); // u2 vs u1 for descending
}
});
System.out.println("Before (by Department then Age Desc):");
sortedByDeptAgeBefore.forEach(System.out::println);
// After: Stream API with Comparator.comparing().thenComparing()
List<User> sortedByDeptAgeAfter = users.stream()
.sorted(Comparator.comparing(User::getDepartment)
.thenComparing(Comparator.comparingInt(User::getAge).reversed()))
.collect(Collectors.toList());
System.out.println("After (by Department then Age Desc):");
sortedByDeptAgeAfter.forEach(System.out::println);
}
}
Clean Code Tip: Comparator.comparing() and thenComparing() for fluent sorting
These static methods on the Comparator interface allow you to build complex sorting logic in a highly readable and fluent way, avoiding nested if statements or anonymous inner classes. Always prefer Comparator.comparing() for its conciseness and clarity.
Exercise & Solution
Exercise: Given a list of Employee objects:
- Sort the employees first by their
departmentalphabetically (ascending). - If employees are in the same department, sort them by
salaryin descending order. - Collect the sorted employees into a new list.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class Employee {
private String name;
private String department;
private double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() {
return name;
}
public String getDepartment() {
return department;
}
public double getSalary() {
return salary;
}
@Override
public String toString() {
return "Employee{" + "name='" + name + '\'' + ", department='" + department + '\'' + ", salary=" + salary + '}';
}
}
public class StreamSortingExercise {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", "HR", 60000.0));
employees.add(new Employee("Bob", "Engineering", 90000.0));
employees.add(new Employee("Charlie", "HR", 75000.0));
employees.add(new Employee("David", "Engineering", 80000.0));
employees.add(new Employee("Eve", "Marketing", 65000.0));
employees.add(new Employee("Frank", "Engineering", 95000.0));
System.out.println("Original Employees:");
employees.forEach(System.out::println);
// Your code here: Sort employees by department (asc) then salary (desc)
List<Employee> sortedEmployees = new ArrayList<>();
// ...
// Solution placeholder
// ...
System.out.println("\nSorted Employees (Department ASC, Salary DESC):");
sortedEmployees.forEach(System.out::println);
}
}
Solution:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class Employee {
private String name;
private String department;
private double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() {
return name;
}
public String getDepartment() {
return department;
}
public double getSalary() {
return salary;
}
@Override
public String toString() {
return "Employee{" + "name='" + name + '\'' + ", department='" + department + '\'' + ", salary=" + salary + '}';
}
}
public class StreamSortingExerciseSolution {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", "HR", 60000.0));
employees.add(new Employee("Bob", "Engineering", 90000.0));
employees.add(new Employee("Charlie", "HR", 75000.0));
employees.add(new Employee("David", "Engineering", 80000.0));
employees.add(new Employee("Eve", "Marketing", 65000.0));
employees.add(new Employee("Frank", "Engineering", 95000.0));
System.out.println("Original Employees:");
employees.forEach(System.out::println);
List<Employee> sortedEmployees = employees.stream()
.sorted(Comparator.comparing(Employee::getDepartment) // Sort by department ascending
.thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed())) // Then by salary descending
.collect(Collectors.toList());
System.out.println("\nSorted Employees (Department ASC, Salary DESC):");
sortedEmployees.forEach(System.out::println);
}
}
Chapter 5: The Optional Class
Technical Theory: The ultimate weapon against NullPointerException
NullPointerException (NPE) is one of the most common and frustrating runtime errors in Java. It occurs when you try to use a reference that points to null as if it were a valid object. Java 8 introduced the Optional<T> class to help developers design APIs that explicitly declare when a value might be absent, forcing consumers of these APIs to handle the absence, thus preventing NPEs.
Optional<T> is a container object that may or may not contain a non-null value.
- If a value is present,
Optionalacts as a wrapper for that value. - If a value is absent, the
Optionalis empty.
Key methods of Optional:
Optional.of(T value): Creates anOptionalwith the given non-null value. ThrowsNullPointerExceptionifvalueisnull.Optional.ofNullable(T value): Creates anOptionalwith the given value, or an emptyOptionalif the value isnull. This is the safer choice when the value might benull.isPresent(): Returnstrueif a value is present,falseotherwise.isEmpty(): Returnstrueif no value is present (Java 11+).get(): Returns the value if present, otherwise throwsNoSuchElementException. Use with caution, similar to a null check.orElse(T other): Returns the value if present, otherwise returnsother(a default value).orElseGet(Supplier<? extends T> other): Returns the value if present, otherwise invokes theSupplierto get a default value. Useful when the default value computation is expensive and should only occur if needed.orElseThrow(Supplier<? extends X> exceptionSupplier): Returns the value if present, otherwise throws an exception created by theSupplier.map(Function<? super T, ? extends U> mapper): If a value is present, applies the mapping function to it, and if the result is non-null, returns anOptionaldescribing the result. Otherwise returns an emptyOptional. This allows chaining transformations without explicit null checks.ifPresent(Consumer<? super T> action): If a value is present, performs the given action with the value, otherwise does nothing.
Professional Code
We'll use a User class with an optional email.
import java.util.Optional;
class User {
private String name;
private Optional<String> email; // Email is optional
public User(String name, String email) {
this.name = name;
this.email = Optional.ofNullable(email); // Use ofNullable to handle potentially null emails
}
public String getName() {
return name;
}
public Optional<String> getEmail() {
return email;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
}
}
public class OptionalExample {
public static void main(String[] args) {
User user1 = new User("Alice", "alice@example.com");
User user2 = new User("Bob", null); // Bob doesn't have an email
// Example 1: Basic usage with .orElse()
System.out.println("--- Handling Optional with .orElse() ---");
// Before: Traditional null check
String email1Before = user1.getEmail().isPresent() ? user1.getEmail().get() : "No email provided";
String email2Before = user2.getEmail().isPresent() ? user2.getEmail().get() : "No email provided";
System.out.println("Alice's email (Before): " + email1Before);
System.out.println("Bob's email (Before): " + email2Before);
// After: Using Optional.orElse()
String email1After = user1.getEmail().orElse("No email provided");
String email2After = user2.getEmail().orElse("No email provided");
System.out.println("Alice's email (After): " + email1After);
System.out.println("Bob's email (After): " + email2After);
// Example 2: Transforming value with .map()
System.out.println("\n--- Transforming Optional with .map() ---");
// Before: Null checks before transformation
String domain1Before = "N/A";
if (user1.getEmail().isPresent()) {
String fullEmail = user1.getEmail().get();
if (fullEmail.contains("@")) {
domain1Before = fullEmail.substring(fullEmail.indexOf("@") + 1);
}
}
System.out.println("Alice's email domain (Before): " + domain1Before);
String domain2Before = "N/A"; // Bob has no email, so domain remains N/A
System.out.println("Bob's email domain (Before): " + domain2Before);
// After: Using Optional.map()
Optional<String> domain1After = user1.getEmail()
.map(email -> email.substring(email.indexOf("@") + 1));
System.out.println("Alice's email domain (After): " + domain1After.orElse("N/A"));
Optional<String> domain2After = user2.getEmail()
.map(email -> email.substring(email.indexOf("@") + 1));
System.out.println("Bob's email domain (After): " + domain2After.orElse("N/A"));
// Example 3: Performing actions with .ifPresent()
System.out.println("\n--- Performing actions with .ifPresent() ---");
// Before: Conditional action
System.out.println("Users with email (Before):");
if (user1.getEmail().isPresent()) {
System.out.println("User " + user1.getName() + " has email: " + user1.getEmail().get());
}
if (user2.getEmail().isPresent()) { // This block won't execute
System.out.println("User " + user2.getName() + " has email: " + user2.getEmail().get());
}
// After: Using Optional.ifPresent()
System.out.println("Users with email (After):");
user1.getEmail().ifPresent(email -> System.out.println("User " + user1.getName() + " has email: " + email));
user2.getEmail().ifPresent(email -> System.out.println("User " + user2.getName() + " has email: " + email)); // Does nothing
}
}
Clean Code Tip: Use Optional for return types where a value might be absent
This makes nullability explicit in your API contracts, forcing callers to consider the absence of a value. Avoid using Optional as a field type or as a method parameter, as it adds unnecessary overhead and doesn't achieve its primary goal of preventing NPEs in those contexts.
Exercise & Solution
Exercise: Create a UserRepository class with a method findUserById(long id) that simulates retrieving a user, which may or may not exist. This method should return Optional<User>.
In your main method:
- Call
findUserByIdfor an existing user (ID 1L) and, if present, print their name and email. If email is not present, print "No Email Provided". - Call
findUserByIdfor a non-existing user (ID 99L) and print "User not found" if absent.
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
class User {
private long id;
private String name;
private Optional<String> email;
public User(long id, String name, String email) {
this.id = id;
this.name = name;
this.email = Optional.ofNullable(email);
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Optional<String> getEmail() {
return email;
}
@Override
public String toString() {
return "User{" + "id=" + id + ", name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
}
}
class UserRepository {
private Map<Long, User> users = new HashMap<>();
public UserRepository() {
users.put(1L, new User(1L, "Alice", "alice@example.com"));
users.put(2L, new User(2L, "Bob", null)); // Bob has no email
users.put(3L, new User(3L, "Charlie", "charlie@example.com"));
}
// Your code here: Method to find user by ID, returning Optional<User>
public Optional<User> findUserById(long id) {
// ...
// Solution placeholder
return Optional.empty(); // Placeholder
// ...
}
}
public class OptionalExercise {
public static void main(String[] args) {
UserRepository userRepository = new UserRepository();
// Scenario 1: Existing user with email
System.out.println("--- Scenario 1: Existing user (ID 1) ---");
// Your code here: Retrieve user 1, print name and email (or "No Email Provided")
// ...
// Scenario 2: Existing user without email (ID 2)
System.out.println("\n--- Scenario 2: Existing user (ID 2, no email) ---");
// Your code here: Retrieve user 2, print name and email (or "No Email Provided")
// ...
// Scenario 3: Non-existing user
System.out.println("\n--- Scenario 3: Non-existing user (ID 99) ---");
// Your code here: Retrieve user 99, print "User not found" if absent
// ...
}
}
Solution:
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
class User {
private long id;
private String name;
private Optional<String> email;
public User(long id, String name, String email) {
this.id = id;
this.name = name;
this.email = Optional.ofNullable(email);
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Optional<String> getEmail() {
return email;
}
@Override
public String toString() {
return "User{" + "id=" + id + ", name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
}
}
class UserRepository {
private Map<Long, User> users = new HashMap<>();
public UserRepository() {
users.put(1L, new User(1L, "Alice", "alice@example.com"));
users.put(2L, new User(2L, "Bob", null)); // Bob has no email
users.put(3L, new User(3L, "Charlie", "charlie@example.com"));
}
public Optional<User> findUserById(long id) {
return Optional.ofNullable(users.get(id));
}
}
public class OptionalExerciseSolution {
public static void main(String[] args) {
UserRepository userRepository = new UserRepository();
// Scenario 1: Existing user with email (ID 1)
System.out.println("--- Scenario 1: Existing user (ID 1) ---");
Optional<User> user1 = userRepository.findUserById(1L);
user1.ifPresent(u -> {
System.out.println("User Name: " + u.getName());
System.out.println("User Email: " + u.getEmail().orElse("No Email Provided"));
});
// Scenario 2: Existing user without email (ID 2)
System.out.println("\n--- Scenario 2: Existing user (ID 2, no email) ---");
Optional<User> user2 = userRepository.findUserById(2L);
user2.ifPresent(u -> {
System.out.println("User Name: " + u.getName());
System.out.println("User Email: " + u.getEmail().orElse("No Email Provided"));
});
// Scenario 3: Non-existing user (ID 99)
System.out.println("\n--- Scenario 3: Non-existing user (ID 99) ---");
Optional<User> user99 = userRepository.findUserById(99L);
System.out.println(user99.map(u -> "Found user: " + u.getName()).orElse("User not found"));
}
}
Chapter 6: Method References
Technical Theory: Cleaning up your lambdas
Method references are a special syntax in Java 8 that provide a shorthand for lambda expressions, making your code even more concise and readable in specific situations. They are used when a lambda expression just calls an existing method. Instead of providing the lambda body, you simply refer to the method by name.
A method reference is of the form ClassName::methodName or objectName::methodName.
There are four main kinds of method references:
-
Static method reference:
ClassName::staticMethodName- Equivalent to
(args) -> ClassName.staticMethodName(args) - Example:
Math::maxfor(a, b) -> Math.max(a, b)
- Equivalent to
-
Instance method reference of a particular object:
objectInstance::instanceMethodName- Equivalent to
(args) -> objectInstance.instanceMethodName(args) - Example:
System.out::printlnfor(s) -> System.out.println(s)
- Equivalent to
-
Instance method reference of an arbitrary object of a particular type:
ClassName::instanceMethodName- Equivalent to
(object, args) -> object.instanceMethodName(args) - This is used when the lambda's first parameter is the target of the instance method.
- Example:
String::lengthfor(s) -> s.length()
- Equivalent to
-
Constructor reference:
ClassName::new- Equivalent to
(args) -> new ClassName(args) - Example:
ArrayList::newfor() -> new ArrayList<>()orInteger::newfor(s) -> new Integer(s)
- Equivalent to
Method references don't introduce new functionality; they just make existing lambdas more compact and often more readable when applicable.
Professional Code
Let's see these in action.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class MethodReferenceExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Example 1: Static method reference (System.out::println, Math::sqrt)
System.out.println("--- Static Method Reference ---");
// Before: Lambda for printing
System.out.println("Printing names (Lambda):");
names.forEach(s -> System.out.println(s));
// After: Method reference for printing
System.out.println("Printing names (Method Reference):");
names.forEach(System.out::println); // Equivalent to (s) -> System.out.println(s)
List<Integer> numbers = Arrays.asList(4, 9, 16, 25);
// Before: Lambda for calculating square root
List<Double> sqrtNumbersBefore = numbers.stream()
.map(num -> Math.sqrt(num))
.collect(Collectors.toList());
System.out.println("Square roots (Lambda): " + sqrtNumbersBefore);
// After: Method reference for Math.sqrt
List<Double> sqrtNumbersAfter = numbers.stream()
.map(Math::sqrt) // Equivalent to (num) -> Math.sqrt(num)
.collect(Collectors.toList());
System.out.println("Square roots (Method Reference): " + sqrtNumbersAfter);
// Example 2: Instance method reference of a particular object
System.out.println("\n--- Instance Method Reference (Specific Object) ---");
// We can create a custom instance and refer to its method
MyPrinter printer = new MyPrinter();
// Before: Lambda using printer instance
System.out.println("Printing with custom printer (Lambda):");
names.forEach(s -> printer.printCustom(s));
// After: Method reference using printer instance
System.out.println("Printing with custom printer (Method Reference):");
names.forEach(printer::printCustom); // Equivalent to (s) -> printer.printCustom(s)
// Example 3: Instance method reference of an arbitrary object of a particular type
System.out.println("\n--- Instance Method Reference (Arbitrary Object of Type) ---");
// Before: Lambda for String::toUpperCase
List<String> upperNamesBefore = names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println("Uppercase names (Lambda): " + upperNamesBefore);
// After: Method reference for String::toUpperCase
List<String> upperNamesAfter = names.stream()
.map(String::toUpperCase) // Equivalent to (s) -> s.toUpperCase()
.collect(Collectors.toList());
System.out.println("Uppercase names (Method Reference): " + upperNamesAfter);
// Before: Lambda for String::length
List<Integer> nameLengthsBefore = names.stream()
.map(s -> s.length())
.collect(Collectors.toList());
System.out.println("Name lengths (Lambda): " + nameLengthsBefore);
// After: Method reference for String::length
List<Integer> nameLengthsAfter = names.stream()
.map(String::length) // Equivalent to (s) -> s.length()
.collect(Collectors.toList());
System.out.println("Name lengths (Method Reference): " + nameLengthsAfter);
// Example 4: Constructor reference
System.out.println("\n--- Constructor Reference ---");
// Before: Lambda for creating a new ArrayList
List<String> newListBefore = numbers.stream()
.map(String::valueOf) // Convert Integer to String
.collect(() -> new ArrayList<>(), List::add, List::addAll); // Old style collect
System.out.println("New List (Lambda constructor): " + newListBefore);
// After: Method reference for creating a new ArrayList (using Collectors.toCollection)
List<String> newListAfter = numbers.stream()
.map(String::valueOf)
.collect(Collectors.toCollection(ArrayList::new)); // Equivalent to () -> new ArrayList<>()
System.out.println("New List (Constructor Reference): " + newListAfter);
}
static class MyPrinter {
public void printCustom(String message) {
System.out.println("Custom Print: " + message);
}
}
}
Clean Code Tip: Prefer method references over lambdas when the lambda body simply invokes an existing method
Method references are more concise and often clearer because they directly state the intent: "apply this method." They remove the slight cognitive overhead of parsing the (args) -> someObject.someMethod(args) syntax when someMethod is exactly what you want to do.
Exercise & Solution
Exercise: Given a list of Strings, convert all of them to uppercase using a method reference. Then, print each string from the new uppercase list to the console, again using a method reference.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MethodReferenceExercise {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "streams");
System.out.println("Original words: " + words);
// Your code here:
// 1. Convert words to uppercase using a method reference.
// Store the result in a new List<String> called 'upperCaseWords'.
List<String> upperCaseWords = new ArrayList<>();
// 2. Print each word from 'upperCaseWords' to the console using a method reference.
System.out.println("\nUppercase words:");
// ...
// Solution placeholder
// ...
}
}
Solution:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MethodReferenceExerciseSolution {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "streams");
System.out.println("Original words: " + words);
// 1. Convert words to uppercase using a method reference.
List<String> upperCaseWords = words.stream()
.map(String::toUpperCase) // Arbitrary object of a particular type
.collect(Collectors.toList());
System.out.println("\nUppercase words:");
// 2. Print each word from 'upperCaseWords' to the console using a method reference.
upperCaseWords.forEach(System.out::println); // Instance method reference of a particular object (System.out)
}
}
Chapter 7: Introduction to Maven
Quick Theory: Why databases are better than .txt files
When it comes to storing application data, plain text files (like .txt or .csv) are highly inefficient and problematic for anything beyond trivial use cases. They offer no inherent structure, making data retrieval, modification, and deletion complex and error-prone. Concurrency control (multiple users accessing at once) is practically non-existent, and data integrity (ensuring data is valid and consistent) must be entirely handled by the application logic, leading to fragile systems.
Relational databases, on the other hand, provide a robust, structured, and efficient solution for data persistence. They offer powerful features like ACID properties (Atomicity, Consistency, Isolation, Durability) to ensure data integrity, built-in query languages (SQL) for efficient data manipulation, and sophisticated mechanisms for concurrency control and user management. Modern applications almost universally rely on databases for their backend storage, guaranteeing reliable and scalable data management.
Maven is a powerful build automation tool used primarily for Java projects. It simplifies the build process by managing dependencies, compiling code, running tests, and packaging applications. The core of a Maven project is the pom.xml (Project Object Model) file, which describes the project's configuration, dependencies, and build lifecycle.
Professional Code
Let's set up a basic pom.xml for our project. We'll include a dependency for SQLite, a lightweight, file-based database, which is excellent for learning and simple applications as it doesn't require a separate server.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.persistence</groupId>
<artifactId>java-persistence-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SQLite JDBC Driver Dependency -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version> <!-- Always use the latest stable version -->
</dependency>
<!-- JUnit 5 for testing (Good practice to include) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Clean Code Tip: Build tools are essential
Always use a build tool like Maven or Gradle for any non-trivial Java project. They automate tedious tasks like dependency management (downloading JARs), compilation, testing, and packaging, ensuring consistency across development environments and significantly reducing manual errors. The pom.xml serves as a central, declarative source of truth for your project's configuration.
Exercise & Solution
Exercise: Create a new directory for a Java project. Inside, create a pom.xml file that:
- Sets the
groupId,artifactId, andversiontocom.mycompany,my-database-app,1.0-SNAPSHOTrespectively. - Configures Java 17 for compilation.
- Includes the
sqlite-jdbcdependency.
<!-- Your pom.xml structure goes here -->
Solution:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>my-database-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SQLite JDBC Driver -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Chapter 8: JDBC Fundamentals
Quick Theory: The Java Database Connectivity (JDBC) API
JDBC (Java Database Connectivity) is a standard Java API for connecting to and interacting with relational databases. It provides a common interface for Java applications to communicate with various database systems (like MySQL, PostgreSQL, Oracle, SQLite), abstracting away the vendor-specific details. This means you can write your database access code once, and it will work with different databases by simply changing the JDBC driver.
The core steps involved in JDBC are often summarized as four stages:
- Load the Driver: Makes the database driver available to the Java application. For modern JDBC (Java 6+),
Class.forName()is often implicit or no longer required for most drivers as they register themselves. - Establish a Connection: Connects the Java application to the database using
DriverManager.getConnection(), providing a JDBC URL, username, and password. - Execute SQL Queries: Creates and executes SQL statements (
StatementorPreparedStatement) to perform CRUD operations. Results are typically retrieved using aResultSet. - Close Resources: Releases database resources (Connection, Statement, ResultSet) to prevent leaks and ensure efficient resource management. This is best handled using try-with-resources.
Professional Code
Let's write a simple Java program to connect to an SQLite database and create a Product table.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcFundamentals {
// Database URL for SQLite. This will create a 'products.db' file in your project root.
private static final String JDBC_URL = "jdbc:sqlite:products.db";
public static void main(String[] args) {
// 1. Load the Driver (No explicit Class.forName() needed for modern JDBC drivers like SQLite)
// The driver typically registers itself when it's loaded onto the classpath by Maven.
System.out.println("Attempting to connect to the database...");
// 2. Establish a Connection & 3. Execute SQL Query & 4. Close Resources
// Using try-with-resources to ensure Connection and Statement are closed automatically.
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
System.out.println("Connection to SQLite established successfully.");
// SQL statement to create the 'products' table if it doesn't already exist.
String createTableSQL = """
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
// Execute the DDL (Data Definition Language) query.
statement.execute(createTableSQL);
System.out.println("Table 'products' checked/created successfully.");
} catch (SQLException e) {
// Catch any SQL exceptions that occur during connection or query execution.
System.err.println("Database error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("Database operation completed. Resources closed.");
}
}
Clean Code Tip: Always close resources with try-with-resources
JDBC resources like Connection, Statement, and ResultSet consume system resources. Failing to close them leads to resource leaks, which can eventually exhaust your application's memory or database connections. The try-with-resources statement (introduced in Java 7) is the cleanest and safest way to ensure these resources are automatically closed, even if exceptions occur.
Exercise & Solution
Exercise: Modify the JdbcFundamentals example to also create a categories table with id (INTEGER PRIMARY KEY AUTOINCREMENT) and name (TEXT NOT NULL) columns, right after creating the products table.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcExercise {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
public static void main(String[] args) {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
System.out.println("Connection to SQLite established successfully.");
String createProductsTableSQL = """
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createProductsTableSQL);
System.out.println("Table 'products' checked/created successfully.");
// Your code here: Add SQL to create the 'categories' table
System.out.println("Table 'categories' checked/created successfully.");
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("Database operation completed. Resources closed.");
}
}
Solution:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcExerciseSolution {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
public static void main(String[] args) {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
System.out.println("Connection to SQLite established successfully.");
String createProductsTableSQL = """
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createProductsTableSQL);
System.out.println("Table 'products' checked/created successfully.");
// Solution: Add SQL to create the 'categories' table
String createCategoriesTableSQL = """
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
""";
statement.execute(createCategoriesTableSQL);
System.out.println("Table 'categories' checked/created successfully.");
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("Database operation completed. Resources closed.");
}
}
Chapter 9: The CRUD Operations (Create, Read, Update, Delete)
Quick Theory: Understanding CRUD
CRUD is an acronym that stands for Create, Read, Update, and Delete. These four basic operations are the fundamental functions of persistent storage and are the bedrock of most database-driven applications. Almost every piece of data you interact with in an application (a user, a product, an order) will at some point undergo one of these operations.
- Create: Adding new data records (e.g.,
INSERTstatements). - Read: Retrieving existing data (e.g.,
SELECTstatements). - Update: Modifying existing data (e.g.,
UPDATEstatements). - Delete: Removing data records (e.g.,
DELETEstatements).
Understanding and implementing these operations efficiently and securely is paramount for any developer working with databases. In this chapter, we'll demonstrate them using basic Statement objects, but remember that for production code, PreparedStatement (covered next) is always the preferred and secure choice.
Professional Code
Let's build a simple Product class and a ProductManager to perform CRUD operations.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// --- Product Model Class ---
class Product {
private int id;
private String name;
private double price;
private int stockQuantity;
// Constructor for creating new products (without ID)
public Product(String name, double price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
// Constructor for retrieving existing products (with ID)
public Product(int id, String name, double price, int stockQuantity) {
this.id = id;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; }
public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
}
}
// --- ProductManager Class for CRUD Operations ---
public class ProductManager {
private static final String JDBC_URL = "jdbc:sqlite:products.db"; // Our database file
// Helper method to get a database connection
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
// CREATE operation
public void addProduct(Product product) {
// SQL query to insert a new product. ID is AUTOINCREMENT, so we don't include it.
// NOTE: For security, PreparedStatements are preferred (see Chapter 10).
String sql = "INSERT INTO products (name, price, stock_quantity) VALUES ('" +
product.getName() + "', " + product.getPrice() + ", " + product.getStockQuantity() + ");";
try (Connection connection = getConnection();
Statement statement = connection.createStatement()) {
int rowsAffected = statement.executeUpdate(sql); // executeUpdate for INSERT, UPDATE, DELETE
if (rowsAffected > 0) {
// To get the auto-generated ID, we would typically use PreparedStatement.RETURN_GENERATED_KEYS.
// For this basic example with Statement, we'll just acknowledge creation.
System.out.println("Product '" + product.getName() + "' added successfully.");
} else {
System.err.println("Failed to add product: " + product.getName());
}
} catch (SQLException e) {
System.err.println("Error adding product: " + e.getMessage());
e.printStackTrace();
}
}
// READ operation - Get all products
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity FROM products;";
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) { // executeQuery for SELECT
while (resultSet.next()) { // Iterate through each row in the result set
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
double price = resultSet.getDouble("price");
int stockQuantity = resultSet.getInt("stock_quantity");
products.add(new Product(id, name, price, stockQuantity));
}
} catch (SQLException e) {
System.err.println("Error retrieving all products: " + e.getMessage());
e.printStackTrace();
}
return products;
}
// READ operation - Get product by ID
public Optional<Product> getProductById(int productId) {
// NOTE: For security, PreparedStatements are preferred (see Chapter 10).
String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = " + productId + ";";
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
if (resultSet.next()) { // If a row is found
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
double price = resultSet.getDouble("price");
int stockQuantity = resultSet.getInt("stock_quantity");
return Optional.of(new Product(id, name, price, stockQuantity));
}
} catch (SQLException e) {
System.err.println("Error retrieving product by ID " + productId + ": " + e.getMessage());
e.printStackTrace();
}
return Optional.empty(); // No product found or an error occurred
}
// UPDATE operation
public void updateProduct(Product product) {
// NOTE: For security, PreparedStatements are preferred (see Chapter 10).
String sql = "UPDATE products SET name = '" + product.getName() + "', " +
"price = " + product.getPrice() + ", " +
"stock_quantity = " + product.getStockQuantity() + " " +
"WHERE id = " + product.getId() + ";";
try (Connection connection = getConnection();
Statement statement = connection.createStatement()) {
int rowsAffected = statement.executeUpdate(sql);
if (rowsAffected > 0) {
System.out.println("Product ID " + product.getId() + " updated successfully.");
} else {
System.out.println("Product ID " + product.getId() + " not found or no changes made.");
}
} catch (SQLException e) {
System.err.println("Error updating product ID " + product.getId() + ": " + e.getMessage());
e.printStackTrace();
}
}
// DELETE operation
public void deleteProduct(int productId) {
// NOTE: For security, PreparedStatements are preferred (see Chapter 10).
String sql = "DELETE FROM products WHERE id = " + productId + ";";
try (Connection connection = getConnection();
Statement statement = connection.createStatement()) {
int rowsAffected = statement.executeUpdate(sql);
if (rowsAffected > 0) {
System.out.println("Product ID " + productId + " deleted successfully.");
} else {
System.out.println("Product ID " + productId + " not found.");
}
} catch (SQLException e) {
System.err.println("Error deleting product ID " + productId + ": " + e.getMessage());
e.printStackTrace();
}
}
// --- Main method to demonstrate CRUD operations ---
public static void main(String[] args) {
ProductManager manager = new ProductManager();
// Ensure the table exists before starting CRUD operations
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
String createTableSQL = """
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createTableSQL);
System.out.println("Products table ensured to exist.\n");
} catch (SQLException e) {
System.err.println("Setup error: " + e.getMessage());
return; // Exit if table creation fails
}
// 1. Create Products
System.out.println("--- Adding Products ---");
manager.addProduct(new Product("Laptop", 1200.00, 10)); // ID will be auto-generated as 1
manager.addProduct(new Product("Mouse", 25.50, 50)); // ID will be auto-generated as 2
manager.addProduct(new Product("Keyboard", 75.00, 20)); // ID will be auto-generated as 3
System.out.println();
// 2. Read All Products
System.out.println("--- All Products ---");
List<Product> allProducts = manager.getAllProducts();
allProducts.forEach(System.out::println);
System.out.println();
// 3. Read a specific Product by ID
System.out.println("--- Product by ID (ID 2) ---");
manager.getProductById(2).ifPresentOrElse(
System.out::println,
() -> System.out.println("Product with ID 2 not found.")
);
System.out.println();
// 4. Update a Product (change Mouse to a Gaming Mouse, reduce stock)
System.out.println("--- Updating Product ID 2 ---");
// We retrieve it first to ensure we have the correct ID
manager.getProductById(2).ifPresent(p -> {
p.setName("Gaming Mouse");
p.setPrice(59.99);
p.setStockQuantity(30);
manager.updateProduct(p);
});
// Verify update
System.out.println("Updated Product ID 2:");
manager.getProductById(2).ifPresent(System.out::println);
System.out.println();
// 5. Delete a Product (delete Keyboard by ID 3)
System.out.println("--- Deleting Product ID 3 ---");
manager.deleteProduct(3);
System.out.println();
// Read all products again to confirm deletion
System.out.println("--- All Products After Deletion ---");
manager.getAllProducts().forEach(System.out::println);
System.out.println();
// Attempt to delete a non-existent product
System.out.println("--- Attempting to Delete Non-existent Product ID 99 ---");
manager.deleteProduct(99);
System.out.println();
}
}
Clean Code Tip: Use constants for SQL queries
Store your SQL queries as private static final String constants. This improves readability, reduces the chance of typos, and makes queries easier to maintain. For complex queries or applications with many queries, externalizing them (e.g., in properties files) can be even better.
Exercise & Solution
Exercise: Based on the ProductManager example, implement a method int getTotalStockValue() that calculates the sum of (price * stock_quantity) for all products in the database.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// (Product class and existing CRUD methods omitted for brevity, assume they are present)
class Product {
private int id;
private String name;
private double price;
private int stockQuantity;
public Product(String name, double price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public Product(int id, String name, double price, int stockQuantity) {
this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; }
public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
}
}
public class ProductManagerExercise {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
// (Existing addProduct, getAllProducts, getProductById, updateProduct, deleteProduct methods omitted)
public void addProduct(Product product) { /* ... */ }
public List<Product> getAllProducts() { /* ... */ return new ArrayList<>(); }
public Optional<Product> getProductById(int productId) { /* ... */ return Optional.empty(); }
public void updateProduct(Product product) { /* ... */ }
public void deleteProduct(int productId) { /* ... */ }
// Your code here: Implement getTotalStockValue()
public double getTotalStockValue() {
double totalValue = 0.0;
// ...
return totalValue;
}
public static void main(String[] args) {
ProductManagerExercise manager = new ProductManagerExercise();
// (Table creation and adding initial products omitted for brevity, assume they are done)
// For testing, let's ensure some data is there:
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
statement.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);");
System.out.println("Test data prepared.\n");
} catch (SQLException e) {
System.err.println("Test setup error: " + e.getMessage());
return;
}
System.out.println("Total stock value: " + manager.getTotalStockValue());
}
}
Solution:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
class Product { // Full Product class for solution context
private int id;
private String name;
private double price;
private int stockQuantity;
public Product(String name, double price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public Product(int id, String name, double price, int stockQuantity) {
this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; }
public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
}
}
public class ProductManagerExerciseSolution {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
// Add back the necessary methods from ProductManager if running standalone
public void addProduct(Product product) {
String sql = "INSERT INTO products (name, price, stock_quantity) VALUES ('" +
product.getName() + "', " + product.getPrice() + ", " + product.getStockQuantity() + ");";
try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
statement.executeUpdate(sql);
} catch (SQLException e) { e.printStackTrace(); }
}
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity FROM products;";
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
while (resultSet.next()) {
products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
}
} catch (SQLException e) { e.printStackTrace(); }
return products;
}
public Optional<Product> getProductById(int productId) {
String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = " + productId + ";";
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
if (resultSet.next()) {
return Optional.of(new Product(resultSet.getInt("id"), resultSet.getString("name"),
resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
}
} catch (SQLException e) { e.printStackTrace(); }
return Optional.empty();
}
public void updateProduct(Product product) {
String sql = "UPDATE products SET name = '" + product.getName() + "', " +
"price = " + product.getPrice() + ", " +
"stock_quantity = " + product.getStockQuantity() + " " +
"WHERE id = " + product.getId() + ";";
try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
statement.executeUpdate(sql);
} catch (SQLException e) { e.printStackTrace(); }
}
public void deleteProduct(int productId) {
String sql = "DELETE FROM products WHERE id = " + productId + ";";
try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
statement.executeUpdate(sql);
} catch (SQLException e) { e.printStackTrace(); }
}
// Solution: Implement getTotalStockValue()
public double getTotalStockValue() {
double totalValue = 0.0;
// We can either retrieve all products and sum them in Java,
// or let the database do the calculation which is usually more efficient.
String sql = "SELECT SUM(price * stock_quantity) AS total_value FROM products;";
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
if (resultSet.next()) { // Expecting only one row with the sum
totalValue = resultSet.getDouble("total_value");
}
} catch (SQLException e) {
System.err.println("Error calculating total stock value: " + e.getMessage());
e.printStackTrace();
}
return totalValue;
}
public static void main(String[] args) {
ProductManagerExerciseSolution manager = new ProductManagerExerciseSolution();
// For testing, let's ensure some data is there:
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
statement.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);"); // Value: 2500
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);"); // Value: 1500
System.out.println("Test data prepared.\n");
} catch (SQLException e) {
System.err.println("Test setup error: " + e.getMessage());
return;
}
System.out.println("Total stock value: " + manager.getTotalStockValue()); // Expected: 4000.0
}
}
Chapter 10: PreparedStatements
Quick Theory: The Necessity of PreparedStatements
Using plain Statement objects for executing SQL queries where user input is directly concatenated into the SQL string is a critical security vulnerability known as SQL Injection. An attacker can inject malicious SQL code through user input, potentially leading to unauthorized data access, modification, or even deletion of entire tables. For instance, if a user inputs ' OR '1'='1 into a login field, a naive Statement might execute SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '', effectively bypassing authentication.
PreparedStatement is the solution to SQL injection. It pre-compiles the SQL query string with ? placeholders for parameters. When you set parameters using methods like setString(), setInt(), setDouble(), etc., the database driver treats these values as literal data, not as executable SQL code. This ensures that no matter what malicious characters are in the user input, they are never interpreted as part of the query's structure, eliminating the risk of injection. PreparedStatement also offers performance benefits by allowing the database to parse and optimize the query once.
Professional Code
Let's refactor our ProductManager to use PreparedStatement for all CRUD operations. This is a mandatory practice for secure and robust database interactions.
Clean Code Tip: PreparedStatements are mandatory for security
Never use Statement for queries involving user input. Always, always use PreparedStatement with ? placeholders for parameters. This is the single most important rule for preventing SQL injection vulnerabilities and ensuring the security and integrity of your database.
Exercise & Solution
Exercise: Given the ProductManagerPreparedStatement class, refactor the getTotalStockValue() method from the previous exercise to use a PreparedStatement (even though it doesn't strictly need parameters, it's good practice for consistency and future parameterization).
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// (Product class and existing CRUD methods omitted for brevity, assume they are present)
class Product {
private int id; private String name; private double price; private int stockQuantity;
public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}
public class PreparedStatementExercise {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
// (Existing addProduct, getAllProducts, getProductById, updateProduct, deleteProduct methods omitted)
// Assume these are implemented using PreparedStatements.
// Your code here: Refactor getTotalStockValue() to use PreparedStatement
public double getTotalStockValue() {
double totalValue = 0.0;
// ...
// Solution Placeholder
// ...
return totalValue;
}
public static void main(String[] args) {
PreparedStatementExercise manager = new PreparedStatementExercise();
// (Test data setup omitted for brevity, assume it's there)
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;");
statement.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);"); // Value: 2500
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);"); // Value: 1500
System.out.println("Test data prepared.\n");
} catch (SQLException e) {
System.err.println("Test setup error: " + e.getMessage());
return;
}
System.out.println("Total stock value: " + manager.getTotalStockValue());
}
}
Solution:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
class Product { // Full Product class for solution context
private int id; private String name; private double price; private int stockQuantity;
public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}
public class PreparedStatementExerciseSolution {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
// Solution: Refactor getTotalStockValue() to use PreparedStatement
public double getTotalStockValue() {
double totalValue = 0.0;
String sql = "SELECT SUM(price * stock_quantity) AS total_value FROM products;";
try (Connection connection = getConnection();
// Even without parameters, using PreparedStatement is good for consistency and slight performance benefit
// if the query is executed multiple times in a real application.
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
totalValue = resultSet.getDouble("total_value");
}
} catch (SQLException e) {
System.err.println("Error calculating total stock value: " + e.getMessage());
e.printStackTrace();
}
return totalValue;
}
public static void main(String[] args) {
PreparedStatementExerciseSolution manager = new PreparedStatementExerciseSolution();
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;");
statement.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);");
System.out.println("Test data prepared.\n");
} catch (SQLException e) {
System.err.println("Test setup error: " + e.getMessage());
return;
}
System.out.println("Total stock value: " + manager.getTotalStockValue());
}
}
Chapter 11: Transaction Management
Technical Theory: Ensuring Atomicity with Transactions
Database transactions are fundamental for maintaining data integrity and consistency, especially when multiple related database operations need to be treated as a single, indivisible unit. The ACID properties (Atomicity, Consistency, Isolation, Durability) are the standard for reliable transaction processing.
- Atomicity: All operations within a transaction either succeed completely, or all of them fail completely. There's no half-way state. If any part of the transaction fails, the entire transaction is rolled back, leaving the database in its original state as if nothing happened.
- Consistency: A transaction brings the database from one valid state to another. It ensures that all data integrity rules (like foreign key constraints, unique constraints) are maintained.
- Isolation: Concurrent transactions execute as if they were running serially. The intermediate state of one transaction is not visible to other concurrent transactions until it is committed.
- Durability: Once a transaction has been committed, its changes are permanent and will survive system failures (like power outages).
In JDBC, Connection objects are in auto-commit mode by default, meaning each SQL statement is treated as a separate transaction and committed immediately. To group multiple statements into a single transaction, you must disable auto-commit using connection.setAutoCommit(false). Then, you explicitly call connection.commit() if all operations succeed, or connection.rollback() if any operation fails.
Professional Code
Let's simulate a scenario where money is transferred between two bank accounts to demonstrate transaction management. If either debit or credit fails, the entire transaction should be rolled back.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionManagement {
private static final String JDBC_URL = "jdbc:sqlite:bank.db"; // A separate DB for bank accounts
// --- Helper methods to manage bank accounts table ---
// Initializes the 'accounts' table and inserts some test data.
private static void setupDatabase() {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS accounts;"); // Start fresh for demo
String createTableSQL = """
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_number TEXT NOT NULL UNIQUE,
balance REAL NOT NULL DEFAULT 0.0
);
""";
statement.execute(createTableSQL);
System.out.println("Accounts table created.");
// Insert test accounts
statement.executeUpdate("INSERT INTO accounts (account_number, balance) VALUES ('ACC001', 1000.00);");
statement.executeUpdate("INSERT INTO accounts (account_number, balance) VALUES ('ACC002', 500.00);");
System.out.println("Test accounts 'ACC001' (1000.00) and 'ACC002' (500.00) created.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
e.printStackTrace();
}
}
// Prints the current balances of all accounts
private static void printAccountBalances() {
System.out.println("--- Current Account Balances ---");
String sql = "SELECT account_number, balance FROM accounts;";
try (Connection connection = DriverManager.getConnection(JDBC_URL);
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("account_number") + ": " + rs.getDouble("balance"));
}
} catch (SQLException e) {
System.err.println("Error retrieving balances: " + e.getMessage());
e.printStackTrace();
}
System.out.println("--------------------------------\n");
}
// --- Transactional Transfer Money Method ---
/**
* Transfers a specified amount from one account to another in a single transaction.
* Ensures atomicity: either both debit and credit succeed, or both fail.
* @param fromAccountNumber The account to debit.
* @param toAccountNumber The account to credit.
* @param amount The amount to transfer.
* @return true if transfer was successful, false otherwise.
*/
public boolean transferMoney(String fromAccountNumber, String toAccountNumber, double amount) {
// We will use a single Connection object for the entire transaction.
// It's crucial that all operations within a transaction use the same connection.
Connection connection = null;
try {
connection = DriverManager.getConnection(JDBC_URL);
connection.setAutoCommit(false); // Disable auto-commit to start a transaction
// 1. Debit from the source account
String debitSql = "UPDATE accounts SET balance = balance - ? WHERE account_number = ? AND balance >= ?;";
try (PreparedStatement debitPs = connection.prepareStatement(debitSql)) {
debitPs.setDouble(1, amount);
debitPs.setString(2, fromAccountNumber);
debitPs.setDouble(3, amount); // Check for sufficient balance
int debitRowsAffected = debitPs.executeUpdate();
if (debitRowsAffected == 0) {
System.out.println("Transfer failed: Insufficient funds or source account not found for " + fromAccountNumber);
connection.rollback(); // Rollback if debit fails (e.g., insufficient funds)
return false;
}
}
// (Optional) Simulate an error here to test rollback
// if (fromAccountNumber.equals("ACC001") && amount == 150.0) {
// throw new SQLException("Simulated error during transfer to test rollback!");
// }
// 2. Credit to the destination account
String creditSql = "UPDATE accounts SET balance = balance + ? WHERE account_number = ?;";
try (PreparedStatement creditPs = connection.prepareStatement(creditSql)) {
creditPs.setDouble(1, amount);
creditPs.setString(2, toAccountNumber);
int creditRowsAffected = creditPs.executeUpdate();
if (creditRowsAffected == 0) {
System.out.println("Transfer failed: Destination account not found for " + toAccountNumber);
connection.rollback(); // Rollback if credit fails (e.g., destination account doesn't exist)
return false;
}
}
connection.commit(); // Commit the transaction if both operations succeed
System.out.println("Transfer of " + amount + " from " + fromAccountNumber + " to " + toAccountNumber + " successful.");
return true;
} catch (SQLException e) {
System.err.println("Transfer failed due to a database error: " + e.getMessage());
if (connection != null) {
try {
System.out.println("Attempting to rollback changes...");
connection.rollback(); // Rollback on any SQLException
System.out.println("Rollback successful.");
} catch (SQLException rollbackEx) {
System.err.println("Error during rollback: " + rollbackEx.getMessage());
}
}
e.printStackTrace();
return false;
} finally {
if (connection != null) {
try {
connection.setAutoCommit(true); // Restore auto-commit mode
connection.close(); // Close the connection
} catch (SQLException closeEx) {
System.err.println("Error closing connection: " + closeEx.getMessage());
}
}
}
}
public static void main(String[] args) {
setupDatabase(); // Initialize our bank accounts database
TransactionManagement manager = new TransactionManagement();
System.out.println("Initial balances:");
printAccountBalances();
// Scenario 1: Successful transfer
System.out.println("--- Attempting successful transfer: ACC001 -> ACC002, $100.00 ---");
manager.transferMoney("ACC001", "ACC002", 100.00);
printAccountBalances(); // Balances should be: ACC001: 900.00, ACC002: 600.00
// Scenario 2: Transfer with insufficient funds (ACC001 has 900, try to transfer 1000)
System.out.println("--- Attempting transfer with insufficient funds: ACC001 -> ACC002, $1000.00 ---");
manager.transferMoney("ACC001", "ACC002", 1000.00);
printAccountBalances(); // Balances should be unchanged: ACC001: 900.00, ACC002: 600.00
// Scenario 3: Transfer to a non-existent account (will trigger rollback)
System.out.println("--- Attempting transfer to non-existent account: ACC001 -> ACC999, $50.00 ---");
manager.transferMoney("ACC001", "ACC999", 50.00);
printAccountBalances(); // Balances should be unchanged: ACC001: 900.00, ACC002: 600.00
}
}
Clean Code Tip: Transactions are for critical multi-step operations
Use transactions whenever multiple database operations must succeed or fail together to maintain data consistency (atomicity). Always disable auto-commit, explicitly call commit() on success, and rollback() on failure (typically within a catch block). Remember to set auto-commit back to true and close the connection in a finally block to prevent resource leaks and unexpected behavior for subsequent database interactions.
Exercise & Solution
Exercise: Implement a method batchUpdateProductPrices(double percentageIncrease) that updates the price of all products by a given percentage. This operation should be transactional. If any error occurs during the update process (e.g., a constraint violation, although less likely with a simple update), all price changes should be rolled back.
For this exercise, you'll need the Product class and basic setup from previous chapters.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
// (Product class and a getConnection helper, setupDatabase method from previous chapters are assumed)
class Product { // Full Product class for solution context
private int id; private String name; private double price; private int stockQuantity;
public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}
public class TransactionExercise {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
private static void setupDatabase() {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
String createTableSQL = """
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createTableSQL);
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Laptop', 1000.00, 10);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Monitor', 300.00, 15);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Webcam', 50.00, 20);");
System.out.println("Products table created with initial data.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
e.printStackTrace();
}
}
// Helper to print all products
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity FROM products;";
try (Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
}
} catch (SQLException e) { e.printStackTrace(); }
return products;
}
// Your code here: Implement batchUpdateProductPrices()
public boolean batchUpdateProductPrices(double percentageIncrease) {
// ...
return false; // Placeholder
}
public static void main(String[] args) {
setupDatabase();
TransactionExercise manager = new TransactionExercise();
System.out.println("Products before update:");
manager.getAllProducts().forEach(System.out::println);
System.out.println("\n--- Attempting 10% price increase ---");
if (manager.batchUpdateProductPrices(0.10)) { // 10% increase
System.out.println("\nProducts after successful update:");
manager.getAllProducts().forEach(System.out::println);
} else {
System.out.println("\nPrice update failed. Products should be unchanged:");
manager.getAllProducts().forEach(System.out::println);
}
System.out.println("\n--- Attempting 20% price increase (and simulate an error) ---");
// To simulate error, you might temporarily introduce a bug in the SQL or trigger a constraint.
// For example, if you set a price to be negative (if allowed by DB, but bad logic).
// Or if you update the SQL string to be invalid: String sql = "UPDATE products SET price_INVALID = price * (1 + ?);";
// This will trigger the rollback.
if (manager.batchUpdateProductPrices(0.20)) {
System.out.println("\nProducts after (unexpectedly) successful update:");
manager.getAllProducts().forEach(System.out::println);
} else {
System.out.println("\nPrice update failed as expected. Products should be unchanged (or rolled back):");
manager.getAllProducts().forEach(System.out::println);
}
}
}
Solution:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
class Product { // Full Product class for solution context
private int id; private String name; private double price; private int stockQuantity;
public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}
public class TransactionExerciseSolution {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
private static void setupDatabase() {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
String createTableSQL = """
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createTableSQL);
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Laptop', 1000.00, 10);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Monitor', 300.00, 15);");
statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Webcam', 50.00, 20);");
System.out.println("Products table created with initial data.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
e.printStackTrace();
}
}
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity FROM products;";
try (Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
}
} catch (SQLException e) { e.printStackTrace(); }
return products;
}
// Solution: Implement batchUpdateProductPrices()
public boolean batchUpdateProductPrices(double percentageIncrease) {
Connection connection = null;
try {
connection = getConnection();
connection.setAutoCommit(false); // Begin transaction
String sql = "UPDATE products SET price = price * (1 + ?);"; // Update all prices
// To simulate an error, you could temporarily change `price * (1 + ?)` to `price_INVALID * (1 + ?)`
// or even just `price_INVALID` to cause a SQL syntax error.
// String sql = "UPDATE products SET price_INVALID = price * (1 + ?);"; // Uncomment to test rollback
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setDouble(1, percentageIncrease);
int rowsAffected = ps.executeUpdate();
System.out.println("Attempted to update prices for " + rowsAffected + " products.");
}
connection.commit(); // Commit if all updates succeed
System.out.println("Batch price update successful by " + (percentageIncrease * 100) + "%.");
return true;
} catch (SQLException e) {
System.err.println("Batch price update failed due to a database error: " + e.getMessage());
if (connection != null) {
try {
System.out.println("Attempting to rollback changes...");
connection.rollback(); // Rollback on any SQL exception
System.out.println("Rollback successful.");
} catch (SQLException rollbackEx) {
System.err.println("Error during rollback: " + rollbackEx.getMessage());
}
}
e.printStackTrace();
return false;
} finally {
if (connection != null) {
try {
connection.setAutoCommit(true); // Restore auto-commit
connection.close(); // Close the connection
} catch (SQLException closeEx) {
System.err.println("Error closing connection: " + closeEx.getMessage());
}
}
}
}
public static void main(String[] args) {
setupDatabase();
TransactionExerciseSolution manager = new TransactionExerciseSolution();
System.out.println("Products before update:");
manager.getAllProducts().forEach(System.out::println);
System.out.println("\n--- Attempting 10% price increase ---");
if (manager.batchUpdateProductPrices(0.10)) { // 10% increase
System.out.println("\nProducts after successful update:");
manager.getAllProducts().forEach(System.out::println);
} else {
System.out.println("\nPrice update failed. Products should be unchanged:");
manager.getAllProducts().forEach(System.out::println);
}
System.out.println("\n--- Attempting 20% price increase (and simulate an error) ---");
// To simulate error for testing:
// Temporarily change the SQL in batchUpdateProductPrices to be invalid, e.g.,
// String sql = "UPDATE products SET price_INVALID = price * (1 + ?);";
// Then run this scenario.
if (manager.batchUpdateProductPrices(0.20)) {
System.out.println("\nProducts after (unexpectedly) successful update:");
manager.getAllProducts().forEach(System.out::println);
} else {
System.out.println("\nPrice update failed as expected. Products should be unchanged (or rolled back):");
manager.getAllProducts().forEach(System.out::println);
}
}
}
Chapter 12: DAO Pattern (Data Access Object)
Technical Theory: Professional Architectural Pattern
The Data Access Object (DAO) pattern is a widely used architectural pattern in enterprise applications to separate the low-level data access logic from the high-level business logic. Its primary goal is to abstract how data is persisted, retrieved, updated, and deleted, allowing the rest of the application to interact with data objects without needing to know the specifics of the underlying database (SQL, JDBC, ORM, etc.).
Benefits of the DAO pattern:
- Separation of Concerns: Business logic doesn't get cluttered with database details. The DAO layer handles all persistence-related operations.
- Easier Maintenance: Changes to the database schema or underlying persistence technology (e.g., switching from JDBC to JPA, or from SQLite to MySQL) only require modifications to the DAO implementation, not the business logic.
- Improved Testability: You can easily mock or substitute DAO implementations for unit testing your business logic without needing a live database connection.
- Reusability: DAO classes (or interfaces) can be reused across different parts of the application or even in different applications.
A typical DAO structure involves:
- Model/Entity Class: Represents the data structure (e.g.,
Product). - DAO Interface: Defines the contract for data operations (e.g.,
ProductDAOwith methods likeadd,getById,update,delete). - DAO Implementation Class: Implements the DAO interface, containing the actual JDBC (or other persistence technology) code. (e.g.,
ProductDAOImpl). - Client/Service Class: Uses the DAO interface to perform business operations, without knowing the implementation details.
Professional Code
Let's refactor our Product CRUD operations into the DAO pattern.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// 1. Model/Entity Class (Product) - No changes from previous chapters
class Product {
private int id;
private String name;
private double price;
private int stockQuantity;
public Product(String name, double price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public Product(int id, String name, double price, int stockQuantity) {
this.id = id;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; }
public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
}
}
// 2. DAO Interface: Defines the contract for Product data access
interface ProductDAO {
void addProduct(Product product);
Optional<Product> getProductById(int id);
List<Product> getAllProducts();
void updateProduct(Product product);
void deleteProduct(int id);
}
// 3. DAO Implementation Class: Contains the JDBC logic
class ProductDAOImpl implements ProductDAO {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
// Helper method to get a database connection
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL);
}
@Override
public void addProduct(Product product) {
String sql = "INSERT INTO products (name, price, stock_quantity) VALUES (?, ?, ?);";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, product.getName());
ps.setDouble(2, product.getPrice());
ps.setInt(3, product.getStockQuantity());
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
if (generatedKeys.next()) {
product.setId(generatedKeys.getInt(1)); // Set the generated ID back to the product
System.out.println("[DAO] Product '" + product.getName() + "' added with ID: " + product.getId());
}
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error adding product: " + e.getMessage());
e.printStackTrace();
}
}
@Override
public Optional<Product> getProductById(int id) {
String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = ?;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(new Product(rs.getInt("id"), rs.getString("name"),
rs.getDouble("price"), rs.getInt("stock_quantity")));
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving product by ID " + id + ": " + e.getMessage());
e.printStackTrace();
}
return Optional.empty();
}
@Override
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity FROM products;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
products.add(new Product(rs.getInt("id"), rs.getString("name"),
rs.getDouble("price"), rs.getInt("stock_quantity")));
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving all products: " + e.getMessage());
e.printStackTrace();
}
return products;
}
@Override
public void updateProduct(Product product) {
String sql = "UPDATE products SET name = ?, price = ?, stock_quantity = ? WHERE id = ?;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, product.getName());
ps.setDouble(2, product.getPrice());
ps.setInt(3, product.getStockQuantity());
ps.setInt(4, product.getId());
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
System.out.println("[DAO] Product ID " + product.getId() + " updated.");
} else {
System.out.println("[DAO] Product ID " + product.getId() + " not found for update.");
}
} catch (SQLException e) {
System.err.println("[DAO] Error updating product ID " + product.getId() + ": " + e.getMessage());
e.printStackTrace();
}
}
@Override
public void deleteProduct(int id) {
String sql = "DELETE FROM products WHERE id = ?;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setInt(1, id);
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
System.out.println("[DAO] Product ID " + id + " deleted.");
} else {
System.out.println("[DAO] Product ID " + id + " not found for deletion.");
}
} catch (SQLException e) {
System.err.println("[DAO] Error deleting product ID " + id + ": " + e.getMessage());
e.printStackTrace();
}
}
}
// 4. Client/Service Class: Uses the DAO interface
public class ProductService { // Renamed from main class to represent a higher-level service
private ProductDAO productDAO; // Depending on the interface, not the implementation
public ProductService(ProductDAO productDAO) {
this.productDAO = productDAO;
}
// Business logic methods that use the DAO
public void createNewProduct(String name, double price, int stock) {
Product product = new Product(name, price, stock);
productDAO.addProduct(product);
System.out.println("[Service] Created product: " + product);
}
public void displayAllProducts() {
System.out.println("\n[Service] --- All Products ---");
List<Product> products = productDAO.getAllProducts();
if (products.isEmpty()) {
System.out.println("No products available.");
} else {
products.forEach(System.out::println);
}
}
public void updateProductDetails(int id, String newName, double newPrice, int newStock) {
Optional<Product> existingProduct = productDAO.getProductById(id);
if (existingProduct.isPresent()) {
Product productToUpdate = existingProduct.get();
productToUpdate.setName(newName);
productToUpdate.setPrice(newPrice);
productToUpdate.setStockQuantity(newStock);
productDAO.updateProduct(productToUpdate);
System.out.println("[Service] Updated product ID " + id);
} else {
System.out.println("[Service] Product ID " + id + " not found for update.");
}
}
public void removeProduct(int id) {
productDAO.deleteProduct(id);
System.out.println("[Service] Attempted to remove product ID " + id);
}
// --- Main method to demonstrate DAO pattern usage ---
public static void main(String[] args) {
// --- Database Setup (can be extracted to a separate utility/init) ---
try (Connection connection = DriverManager.getConnection("jdbc:sqlite:products.db");
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh demo
String createTableSQL = """
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL
);
""";
statement.execute(createTableSQL);
System.out.println("Products table ensured to exist and cleared.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
return;
}
// --- End Database Setup ---
// Inject the DAO implementation into the service
ProductDAO productDAO = new ProductDAOImpl();
ProductService productService = new ProductService(productDAO);
// Perform operations via the Service layer
productService.createNewProduct("Laptop", 1200.00, 10); // ID: 1
productService.createNewProduct("Mouse", 25.50, 50); // ID: 2
productService.createNewProduct("Keyboard", 75.00, 20); // ID: 3
productService.displayAllProducts();
productService.updateProductDetails(2, "Wireless Mouse", 35.99, 45); // Update ID 2
productDAO.getProductById(2).ifPresent(p -> System.out.println("Verified update: " + p));
productService.removeProduct(3); // Delete ID 3
productService.removeProduct(99); // Attempt to delete non-existent
productService.displayAllProducts();
}
}
Clean Code Tip: DAO decouples business logic from persistence Always implement the DAO pattern (or use an ORM like Hibernate which provides its own abstraction) to separate your application's business logic from its persistence logic. This promotes modularity, makes your code more robust to changes in database technology, and drastically improves testability. Depend on the DAO interface, not the concrete implementation.
Exercise & Solution
Exercise: Extend the DAO pattern to include a Category entity.
- Create a
Categorymodel class (id,name). - Create a
CategoryDAOinterface andCategoryDAOImplclass (withadd,getById,getAll). - Modify the
productstable to include acategory_id(INTEGER, REFERENCES categories(id)) column. - Update
Productmodel to include acategoryIdfield. - Update
ProductDAOmethods (addProduct,getProductById,getAllProducts) andProductDAOImplto handle thecategory_id. - Demonstrate usage in
mainby adding categories first, then products associated with those categories.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// --- 1. Category Model Class ---
class Category {
private int id;
private String name;
public Category(String name) { this.name = name; }
public Category(int id, String name) { this.id = id; this.name = name; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
@Override public String toString() { return "Category{id=" + id + ", name='" + name + "'}"; }
}
// --- Product Model Class (Modified) ---
class Product {
private int id; private String name; private double price; private int stockQuantity; private int categoryId;
public Product(String name, double price, int stockQuantity, int categoryId) {
this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
}
public Product(int id, String name, double price, int stockQuantity, int categoryId) {
this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
}
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; }
@Override public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + ", categoryId=" + categoryId + '}';
}
}
// --- 2. CategoryDAO Interface ---
interface CategoryDAO {
void addCategory(Category category);
Optional<Category> getCategoryById(int id);
List<Category> getAllCategories();
}
// --- ProductDAO Interface (Modified) ---
interface ProductDAO {
void addProduct(Product product);
Optional<Product> getProductById(int id);
List<Product> getAllProducts();
void updateProduct(Product product);
void deleteProduct(int id);
}
// --- 3. CategoryDAOImpl Class ---
class CategoryDAOImpl implements CategoryDAO {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }
@Override public void addCategory(Category category) { /* ... */ }
@Override public Optional<Category> getCategoryById(int id) { /* ... */ return Optional.empty(); }
@Override public List<Category> getAllCategories() { /* ... */ return new ArrayList<>(); }
}
// --- ProductDAOImpl Class (Modified) ---
class ProductDAOImpl implements ProductDAO {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }
@Override public void addProduct(Product product) { /* ... */ }
@Override public Optional<Product> getProductById(int id) { /* ... */ return Optional.empty(); }
@Override public List<Product> getAllProducts() { /* ... */ return new ArrayList<>(); }
@Override public void updateProduct(Product product) { /* ... */ }
@Override public void deleteProduct(int id) { /* ... */ }
}
// --- Client/Service Class (Modified Main) ---
public class DaoExercise {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private static void setupDatabase() {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;");
statement.execute("DROP TABLE IF EXISTS categories;");
String createCategoriesTableSQL = """
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
""";
statement.execute(createCategoriesTableSQL);
System.out.println("Categories table created.");
String createProductsTableSQL = """
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);
""";
statement.execute(createProductsTableSQL);
System.out.println("Products table created.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
setupDatabase();
CategoryDAO categoryDAO = new CategoryDAOImpl();
ProductDAO productDAO = new ProductDAOImpl();
// Your code here:
// 1. Add some categories (e.g., "Electronics", "Books")
// 2. Retrieve their IDs
// 3. Add products, associating them with categories
// 4. Display all categories and products
System.out.println("\n--- All Categories ---");
categoryDAO.getAllCategories().forEach(System.out::println);
System.out.println("\n--- All Products ---");
productDAO.getAllProducts().forEach(System.out::println);
}
}
Solution:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// --- 1. Category Model Class ---
class Category {
private int id;
private String name;
public Category(String name) { this.name = name; }
public Category(int id, String name) { this.id = id; this.name = name; }
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
@Override public String toString() { return "Category{id=" + id + ", name='" + name + "'}"; }
}
// --- Product Model Class (Modified) ---
class Product {
private int id; private String name; private double price; private int stockQuantity; private int categoryId; // Added categoryId
public Product(String name, double price, int stockQuantity, int categoryId) {
this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
}
public Product(int id, String name, double price, int stockQuantity, int categoryId) {
this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
}
public int getId() { return id; } public void setId(int id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; }
@Override public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + ", categoryId=" + categoryId + '}';
}
}
// --- 2. CategoryDAO Interface ---
interface CategoryDAO {
void addCategory(Category category);
Optional<Category> getCategoryById(int id);
List<Category> getAllCategories();
}
// --- ProductDAO Interface (Modified) ---
interface ProductDAO {
void addProduct(Product product);
Optional<Product> getProductById(int id);
List<Product> getAllProducts();
void updateProduct(Product product);
void deleteProduct(int id);
}
// --- 3. CategoryDAOImpl Class ---
class CategoryDAOImpl implements CategoryDAO {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }
@Override
public void addCategory(Category category) {
String sql = "INSERT INTO categories (name) VALUES (?);";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, category.getName());
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
if (generatedKeys.next()) {
category.setId(generatedKeys.getInt(1));
System.out.println("[DAO] Category '" + category.getName() + "' added with ID: " + category.getId());
}
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error adding category: " + e.getMessage());
e.printStackTrace();
}
}
@Override
public Optional<Category> getCategoryById(int id) {
String sql = "SELECT id, name FROM categories WHERE id = ?;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(new Category(rs.getInt("id"), rs.getString("name")));
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving category by ID " + id + ": " + e.getMessage());
e.printStackTrace();
}
return Optional.empty();
}
@Override
public List<Category> getAllCategories() {
List<Category> categories = new ArrayList<>();
String sql = "SELECT id, name FROM categories;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
categories.add(new Category(rs.getInt("id"), rs.getString("name")));
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving all categories: " + e.getMessage());
e.printStackTrace();
}
return categories;
}
}
// --- ProductDAOImpl Class (Modified) ---
class ProductDAOImpl implements ProductDAO {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }
@Override
public void addProduct(Product product) {
String sql = "INSERT INTO products (name, price, stock_quantity, category_id) VALUES (?, ?, ?, ?);"; // Added category_id
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, product.getName());
ps.setDouble(2, product.getPrice());
ps.setInt(3, product.getStockQuantity());
ps.setInt(4, product.getCategoryId()); // Set category_id
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
if (generatedKeys.next()) {
product.setId(generatedKeys.getInt(1));
System.out.println("[DAO] Product '" + product.getName() + "' added with ID: " + product.getId());
}
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error adding product: " + e.getMessage());
e.printStackTrace();
}
}
@Override
public Optional<Product> getProductById(int id) {
String sql = "SELECT id, name, price, stock_quantity, category_id FROM products WHERE id = ?;"; // Added category_id
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(new Product(rs.getInt("id"), rs.getString("name"),
rs.getDouble("price"), rs.getInt("stock_quantity"),
rs.getInt("category_id"))); // Retrieve category_id
}
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving product by ID " + id + ": " + e.getMessage());
e.printStackTrace();
}
return Optional.empty();
}
@Override
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock_quantity, category_id FROM products;"; // Added category_id
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
products.add(new Product(rs.getInt("id"), rs.getString("name"),
rs.getDouble("price"), rs.getInt("stock_quantity"),
rs.getInt("category_id"))); // Retrieve category_id
}
} catch (SQLException e) {
System.err.println("[DAO] Error retrieving all products: " + e.getMessage());
e.printStackTrace();
}
return products;
}
@Override
public void updateProduct(Product product) {
String sql = "UPDATE products SET name = ?, price = ?, stock_quantity = ?, category_id = ? WHERE id = ?;"; // Added category_id
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, product.getName());
ps.setDouble(2, product.getPrice());
ps.setInt(3, product.getStockQuantity());
ps.setInt(4, product.getCategoryId()); // Set category_id
ps.setInt(5, product.getId());
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
System.out.println("[DAO] Product ID " + product.getId() + " updated.");
} else {
System.out.println("[DAO] Product ID " + product.getId() + " not found for update.");
}
} catch (SQLException e) {
System.err.println("[DAO] Error updating product ID " + product.getId() + ": " + e.getMessage());
e.printStackTrace();
}
}
@Override
public void deleteProduct(int id) {
String sql = "DELETE FROM products WHERE id = ?;";
try (Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setInt(1, id);
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
System.out.println("[DAO] Product ID " + id + " deleted.");
} else {
System.out.println("[DAO] Product ID " + id + " not found for deletion.");
}
} catch (SQLException e) {
System.err.println("[DAO] Error deleting product ID " + id + ": " + e.getMessage());
e.printStackTrace();
}
}
}
// --- Client/Service Class (Modified Main) ---
public class DaoExerciseSolution {
private static final String JDBC_URL = "jdbc:sqlite:products.db";
private static void setupDatabase() {
try (Connection connection = DriverManager.getConnection(JDBC_URL);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS products;");
statement.execute("DROP TABLE IF EXISTS categories;");
String createCategoriesTableSQL = """
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
""";
statement.execute(createCategoriesTableSQL);
System.out.println("Categories table created.");
String createProductsTableSQL = """
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock_quantity INTEGER NOT NULL,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);
""";
statement.execute(createProductsTableSQL);
System.out.println("Products table created.\n");
} catch (SQLException e) {
System.err.println("Database setup error: " + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
setupDatabase();
CategoryDAO categoryDAO = new CategoryDAOImpl();
ProductDAO productDAO = new ProductDAOImpl();
// 1. Add some categories
Category electronics = new Category("Electronics");
categoryDAO.addCategory(electronics); // ID will be set to 1
Category books = new Category("Books");
categoryDAO.addCategory(books); // ID will be set to 2
Category homeGoods = new Category("Home Goods");
categoryDAO.addCategory(homeGoods); // ID will be set to 3
System.out.println("\n--- All Categories ---");
categoryDAO.getAllCategories().forEach(System.out::println);
// 2. Add products, associating them with categories
productDAO.addProduct(new Product("Laptop", 1200.00, 10, electronics.getId()));
productDAO.addProduct(new Product("Mouse", 25.50, 50, electronics.getId()));
productDAO.addProduct(new Product("The Hobbit", 15.00, 100, books.getId()));
productDAO.addProduct(new Product("Coffee Maker", 75.00, 15, homeGoods.getId()));
productDAO.addProduct(new Product("Advanced Java", 45.00, 30, books.getId()));
productDAO.addProduct(new Product("Mystery Novel", 12.00, 80, books.getId()));
System.out.println("\n--- All Products ---");
productDAO.getAllProducts().forEach(System.out::println);
// Example: Update a product's category
System.out.println("\n--- Updating Mouse category ---");
Optional<Product> mouse = productDAO.getProductById(2);
mouse.ifPresent(p -> {
p.setCategoryId(homeGoods.getId()); // Change mouse to Home Goods category (just for demo)
productDAO.updateProduct(p);
});
System.out.println("\n--- All Products After Update ---");
productDAO.getAllProducts().forEach(System.out::println);
}
}
Chapter 19: Introduction to JavaFX
Quick Theory: The Visual Approach
Desktop applications require a visual interface for user interaction. Historically, Java's primary GUI toolkit was Swing, but it has largely been superseded. Swing applications often suffered from an outdated look and feel, performance issues, and complex API design, making them challenging to develop and maintain in a modern context. While still functional, Swing is considered a legacy technology.
JavaFX emerged as the modern, high-performance, and feature-rich platform for building rich client applications in Java. It leverages hardware-accelerated graphics, offers a cleaner API, and supports modern UI concepts like CSS styling, declarative UI with FXML, and media playback. JavaFX provides a robust framework for creating visually appealing and responsive desktop applications that can run across various operating systems.
Professional Code
Let's set up a basic JavaFX application, understanding the core components: Stage, Scene, and Node.
Example 1: Basic "Hello World" JavaFX Application
This example shows the minimal setup for a JavaFX application that displays "Hello, JavaFX!" in a window.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorldFX extends Application {
// The start method is the main entry point for all JavaFX applications.
// The primary Stage is the top-level container for a JavaFX application.
@Override
public void start(Stage primaryStage) {
// 1. Create a root node for the scene graph. StackPane is a simple layout
// manager that centers its children.
StackPane root = new StackPane();
// 2. Create a UI control (a Label in this case) to display text.
Label helloLabel = new Label("Hello, JavaFX!");
// 3. Add the label to the root layout pane.
root.getChildren().add(helloLabel);
// 4. Create a Scene, which is the container for all content in a scene graph.
// A Scene is attached to a Stage. We specify the root node and initial dimensions.
Scene scene = new Scene(root, 300, 200);
// 5. Set the title of the primary Stage (the window).
primaryStage.setTitle("My First JavaFX App");
// 6. Set the scene on the primary Stage.
primaryStage.setScene(scene);
// 7. Show the Stage (make the window visible).
primaryStage.show();
}
// The main method is the standard entry point for Java applications.
// It calls Application.launch() which handles JavaFX initialization and calls the start method.
public static void main(String[] args) {
launch(args);
}
}
Example 2: Customizing the Window with Background Color
This example builds on the first, showing how to set a custom background color for the scene and incorporate basic styling.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; // Import Color class
import javafx.stage.Stage;
public class CustomWindowFX extends Application {
@Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Label welcomeLabel = new Label("Welcome to Custom JavaFX!");
// Add some basic CSS styling directly to the label
welcomeLabel.setStyle("-fx-font-size: 24px; -fx-text-fill: white;");
root.getChildren().add(welcomeLabel);
// Create a Scene with a specific background color
// The third argument to the Scene constructor can be a Paint object (e.g., Color.LIGHTBLUE)
Scene scene = new Scene(root, 400, 250, Color.DARKBLUE); // Set scene background to DARKBLUE
primaryStage.setTitle("Custom JavaFX Window");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clean Code Tip: Start small, understand core concepts
When diving into GUI frameworks like JavaFX, resist the urge to build complex UIs immediately. Master the fundamental concepts of Stage (the window), Scene (the content inside), and the Node hierarchy (components and their arrangement) first. Build simple "Hello World" examples, then incrementally add features to solidify your understanding.
Exercise & Solution
Exercise: Create a JavaFX application that displays a window with the title "My Profile" and a Label that says "User: [Your Name]". The window should be 500x150 pixels.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ProfileWindowExercise extends Application {
@Override
public void start(Stage primaryStage) {
// Your code here:
// 1. Create a StackPane as the root layout.
// 2. Create a Label with your name.
// 3. Add the label to the root.
// 4. Create a Scene with the root and dimensions 500x150.
// 5. Set the Stage title to "My Profile".
// 6. Set the scene on the primary stage.
// 7. Show the primary stage.
}
public static void main(String[] args) {
launch(args);
}
}
Solution:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ProfileWindowExerciseSolution extends Application {
@Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Label profileLabel = new Label("User: Jane Doe"); // Replace with your name
profileLabel.setStyle("-fx-font-size: 20px; -fx-text-fill: #333;");
root.getChildren().add(profileLabel);
Scene scene = new Scene(root, 500, 150);
primaryStage.setTitle("My Profile");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Chapter 20: Event Handling (Lambdas)
Quick Theory: Making Your UI Interactive
Graphical User Interfaces are inherently event-driven. Users interact with UI components (buttons, text fields, menus), and these interactions trigger events. To make your UI dynamic and responsive, you need to handle these events by attaching event handlers or listeners to components. An event handler is a piece of code that executes when a specific event occurs.
JavaFX, with its modern API design, fully embraces functional programming and lambda expressions for event handling. This dramatically simplifies the syntax compared to the older, more verbose anonymous inner classes used in Swing. Instead of defining a separate class or an anonymous class for each event listener, you can provide the event-handling logic directly as a lambda expression, making your code more concise and readable.
Professional Code
Let's see how to use lambdas to make buttons perform actions and update UI elements.
This demonstrates a simple button that, when clicked, prints a message to the console using a lambda.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class EventHandlingConsole extends Application {
@Override
public void start(Stage primaryStage) {
Button clickMeButton = new Button("Click Me!");
// Set an action for the button using a lambda expression.
// The lambda takes an ActionEvent object 'e' (or just 'event' or even omit if not used)
// and defines the code to execute when the button is clicked.
clickMeButton.setOnAction(e -> {
System.out.println("Button was clicked! Event: " + e.getEventType());
});
StackPane root = new StackPane();
root.getChildren().add(clickMeButton);
Scene scene = new Scene(root, 300, 150);
primaryStage.setTitle("Event Handling Demo (Console)");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This example shows how a button click can dynamically change the text of another UI component (a Label).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox; // Using VBox for vertical alignment
import javafx.geometry.Pos; // For alignment
import javafx.stage.Stage;
public class EventHandlingUpdateUI extends Application {
// Declare the Label as a field so it can be accessed and updated from the event handler.
private Label messageLabel;
private int clickCount = 0; // To track clicks
@Override
public void start(Stage primaryStage) {
// Initialize the Label
messageLabel = new Label("Click the button below!");
messageLabel.setStyle("-fx-font-size: 18px; -fx-text-fill: #007bff;");
Button updateButton = new Button("Update Message");
// Lambda expression for the button's action.
// It updates the text of 'messageLabel' and the click count.
updateButton.setOnAction(event -> {
clickCount++;
messageLabel.setText("Button clicked " + clickCount + " time(s)!");
System.out.println("Label updated.");
});
// VBox layout manager to stack components vertically
VBox root = new VBox(20); // 20 pixels spacing between children
root.setAlignment(Pos.CENTER); // Center the children in the VBox
root.getChildren().addAll(messageLabel, updateButton);
Scene scene = new Scene(root, 400, 200);
primaryStage.setTitle("Event Handling Demo (UI Update)");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clean Code Tip: Keep event handlers concise, delegate complex logic Event handlers (especially lambdas) should be lightweight and focused on triggering actions, not performing complex business logic themselves. If an action requires significant computation or multiple steps, delegate that work to a separate method or a dedicated service class. This keeps your UI code clean, readable, and easier to test.
Exercise & Solution
Exercise: Create a JavaFX application with a Label showing "Current Count: 0" and two Buttons: "Increment" and "Decrement".
- Clicking "Increment" should increase the count shown in the label.
- Clicking "Decrement" should decrease the count shown in the label.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.geometry.Pos;
import javafx.stage.Stage;
public class CounterAppExercise extends Application {
private Label countLabel;
private int currentCount = 0;
@Override
public void start(Stage primaryStage) {
countLabel = new Label("Current Count: " + currentCount);
countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
Button incrementButton = new Button("Increment");
Button decrementButton = new Button("Decrement");
// Your code here: Add setOnAction for incrementButton and decrementButton using lambdas.
HBox buttonBox = new HBox(10); // 10 pixels spacing between buttons
buttonBox.setAlignment(Pos.CENTER);
buttonBox.getChildren().addAll(incrementButton, decrementButton);
VBox root = new VBox(20);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(countLabel, buttonBox);
Scene scene = new Scene(root, 300, 180);
primaryStage.setTitle("Counter App");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Solution:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.geometry.Pos;
import javafx.stage.Stage;
public class CounterAppExerciseSolution extends Application {
private Label countLabel;
private int currentCount = 0;
@Override
public void start(Stage primaryStage) {
countLabel = new Label("Current Count: " + currentCount);
countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
Button incrementButton = new Button("Increment");
// Lambda for incrementing the count
incrementButton.setOnAction(e -> {
currentCount++;
countLabel.setText("Current Count: " + currentCount);
});
Button decrementButton = new Button("Decrement");
// Lambda for decrementing the count
decrementButton.setOnAction(e -> {
currentCount--;
countLabel.setText("Current Count: " + currentCount);
});
HBox buttonBox = new HBox(10);
buttonBox.setAlignment(Pos.CENTER);
buttonBox.getChildren().addAll(incrementButton, decrementButton);
VBox root = new VBox(20);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(countLabel, buttonBox);
Scene scene = new Scene(root, 300, 180);
primaryStage.setTitle("Counter App");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Chapter 21: Layout Managers (VBox/HBox/GridPane)
Quick Theory: Arranging Components Visually
Designing a user interface isn't just about placing components; it's about arranging them effectively and ensuring they adapt gracefully to different window sizes and screen resolutions. Hardcoding pixel positions (absolute positioning) is generally a bad practice in modern GUI development because it leads to inflexible UIs that break when elements change size or the window is resized.
JavaFX (like other GUI toolkits) provides layout managers (or layout panes) to handle the positioning and sizing of UI components dynamically. These layout panes follow specific rules to organize their children, allowing for responsive and adaptable interfaces. Key layout managers include VBox (vertical stacking), HBox (horizontal stacking), and GridPane (table-like arrangement), among others. Using these greatly simplifies UI construction and improves maintainability.
Professional Code
Let's explore VBox, HBox, and GridPane for organizing UI elements.
Example 1: Using VBox and HBox for Basic Layouts
This example combines VBox and HBox to create a window with a label at the top and a row of buttons at the bottom.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class LayoutBasics extends Application {
@Override
public void start(Stage primaryStage) {
// --- Top section: A single Label ---
Label headerLabel = new Label("Welcome to the App!");
headerLabel.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
// --- Bottom section: Two buttons horizontally arranged ---
Button saveButton = new Button("Save");
saveButton.setOnAction(e -> System.out.println("Save clicked!"));
Button cancelButton = new Button("Cancel");
cancelButton.setOnAction(e -> System.out.println("Cancel clicked!"));
// HBox to arrange buttons horizontally
HBox buttonBox = new HBox(10); // 10 pixels spacing between children
buttonBox.getChildren().addAll(saveButton, cancelButton);
buttonBox.setAlignment(Pos.CENTER); // Center buttons horizontally within the HBox
// --- Main layout: VBox to stack header and buttonBox vertically ---
VBox root = new VBox(30); // 30 pixels spacing between children
root.setAlignment(Pos.TOP_CENTER); // Align children to top-center of the VBox
root.setPadding(new Insets(20)); // Add 20 pixels padding around the VBox content
root.getChildren().addAll(headerLabel, buttonBox);
Scene scene = new Scene(root, 400, 250);
primaryStage.setTitle("VBox & HBox Layout");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Example 2: Using GridPane for a Form Layout
GridPane is excellent for arranging components in a grid, like a typical form with labels and input fields.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class GridPaneForm extends Application {
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER); // Center the grid in the scene
grid.setHgap(10); // Horizontal gap between columns
grid.setVgap(10); // Vertical gap between rows
grid.setPadding(new Insets(25, 25, 25, 25)); // Padding around the grid
// --- Add components to the grid ---
// Row 0, Column 0: Label for User Name
Label userNameLabel = new Label("User Name:");
grid.add(userNameLabel, 0, 0); // (node, col, row)
// Row 0, Column 1: TextField for User Name
TextField userTextField = new TextField();
userTextField.setPromptText("Enter your username");
grid.add(userTextField, 1, 0);
// Row 1, Column 0: Label for Password
Label passwordLabel = new Label("Password:");
grid.add(passwordLabel, 0, 1);
// Row 1, Column 1: PasswordField (TextField variant for passwords)
TextField passwordField = new TextField(); // Use PasswordField in real app
passwordField.setPromptText("Enter your password");
grid.add(passwordField, 1, 1);
// Row 2, Column 1: Login Button (spanning multiple columns if needed)
Button loginButton = new Button("Login");
// Login button action:
loginButton.setOnAction(e -> {
String username = userTextField.getText();
String password = passwordField.getText();
System.out.println("Attempting login with Username: " + username + ", Password: " + password);
// In a real app, this would involve authentication logic
});
grid.add(loginButton, 1, 2); // Add button to Column 1, Row 2
Scene scene = new Scene(grid, 350, 250); // Set scene size
primaryStage.setTitle("Login Form (GridPane)");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clean Code Tip: Favor declarative layouts over hardcoded coordinates
Always use JavaFX's layout panes (VBox, HBox, GridPane, BorderPane, AnchorPane, etc.) to arrange your UI components. Avoid setting explicit x, y coordinates, width, or height values unless absolutely necessary. Declarative layouts automatically handle component resizing and positioning, making your UI responsive, flexible, and much easier to maintain across different screen sizes and resolutions.
Exercise & Solution
Exercise: Create a simple "Registration Form" using a GridPane. It should have Labels and TextFields for:
- First Name
- Last Name
- A "Register"
Button. Arrange these components neatly within the grid.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class RegistrationFormExercise extends Application {
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
// Your code here: Add Labels and TextFields for First Name, Last Name, Email.
// Also add a Register Button.
// Row 0: First Name
// ...
// Row 1: Last Name
// ...
// Row 2: Email
// ...
// Row 3: Register Button
// ...
Scene scene = new Scene(grid, 400, 300);
primaryStage.setTitle("Registration Form");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Solution:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class RegistrationFormExerciseSolution extends Application {
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
// Row 0: First Name
Label firstNameLabel = new Label("First Name:");
grid.add(firstNameLabel, 0, 0);
TextField firstNameField = new TextField();
firstNameField.setPromptText("John");
grid.add(firstNameField, 1, 0);
// Row 1: Last Name
Label lastNameLabel = new Label("Last Name:");
grid.add(lastNameLabel, 0, 1);
TextField lastNameField = new TextField();
lastNameField.setPromptText("Doe");
grid.add(lastNameField, 1, 1);
// Row 2: Email
Label emailLabel = new Label("Email:");
grid.add(emailLabel, 0, 2);
TextField emailField = new TextField();
emailField.setPromptText("john.doe@example.com");
grid.add(emailField, 1, 2);
// Row 3: Register Button
Button registerButton = new Button("Register");
registerButton.setOnAction(e -> {
String firstName = firstNameField.getText();
String lastName = lastNameField.getText();
String email = emailField.getText();
System.out.println("Registering: " + firstName + " " + lastName + " (" + email + ")");
// In a real application, you'd send this data to a service/database
});
grid.add(registerButton, 1, 3);
Scene scene = new Scene(grid, 400, 300);
primaryStage.setTitle("Registration Form");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Chapter 22: MVC Pattern in GUI
Quick Theory: Separating GUI Logic from Data
Building complex GUIs without a proper architectural pattern can quickly lead to spaghetti code, where business logic is intertwined with UI concerns. This makes the application difficult to maintain, extend, and test. The Model-View-Controller (MVC) pattern addresses this by separating an application into three interconnected components:
- Model: Represents the application's data and business logic. It's independent of the user interface. It notifies the View (or Controller) when its data changes.
- View: Responsible for displaying the Model's data to the user. It's the visual representation of the application and typically has no knowledge of the Model's internal structure or how it processes data. It also sends user input to the Controller.
- Controller: Acts as an intermediary between the Model and the View. It receives user input from the View, processes it (potentially updating the Model), and then updates the View to reflect any changes in the Model.
In a GUI context, MVC ensures a clear separation of concerns, making the codebase more modular, reusable, and testable.
Professional Code
Let's refactor our simple Counter App to demonstrate a basic MVC structure. For simplicity, we'll implement it programmatically without FXML.
Example 1: Counter Application with MVC
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
// --- 1. Model: Holds the application's data and logic ---
class CounterModel {
private int count;
public CounterModel() {
this.count = 0;
}
public int getCount() {
return count;
}
public void increment() {
count++;
}
public void decrement() {
count--;
}
}
// --- 2. View: Displays the UI and sends user input to the Controller ---
// (In a real app, this might be split into an interface and implementation, or use FXML)
class CounterView {
private Label countLabel;
private Button incrementButton;
private Button decrementButton;
private VBox root;
public CounterView() {
// Initialize UI components
countLabel = new Label("Current Count: 0");
countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
incrementButton = new Button("Increment");
decrementButton = new Button("Decrement");
HBox buttonBox = new HBox(10);
buttonBox.setAlignment(Pos.CENTER);
buttonBox.getChildren().addAll(incrementButton, decrementButton);
root = new VBox(20);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(countLabel, buttonBox);
}
public VBox getRoot() {
return root;
}
// Methods to update the view based on model changes
public void updateCountDisplay(int newCount) {
countLabel.setText("Current Count: " + newCount);
}
// Getters for buttons, so Controller can attach event handlers
public Button getIncrementButton() {
return incrementButton;
}
public Button getDecrementButton() {
return decrementButton;
}
}
// --- 3. Controller: Handles user input, updates the Model, and updates the View ---
class CounterController {
private CounterModel model;
private CounterView view;
public CounterController(CounterModel model, CounterView view) {
this.model = model;
this.view = view;
initView(); // Initialize the view with initial model data
attachEventHandlers(); // Attach event handlers to view components
}
private void initView() {
// Ensure the view displays the initial state of the model
view.updateCountDisplay(model.getCount());
}
private void attachEventHandlers() {
view.getIncrementButton().setOnAction(e -> handleIncrement());
view.getDecrementButton().setOnAction(e -> handleDecrement());
}
// Event handling methods
private void handleIncrement() {
model.increment(); // Update the model
view.updateCountDisplay(model.getCount()); // Update the view
System.out.println("Incremented to: " + model.getCount());
}
private void handleDecrement() {
model.decrement(); // Update the model
view.updateCountDisplay(model.getCount()); // Update the view
System.out.println("Decremented to: " + model.getCount());
}
}
// --- Main Application Class ---
public class MvcCounterApp extends Application {
@Override
public void start(Stage primaryStage) {
// Instantiate Model, View, and Controller
CounterModel model = new CounterModel();
CounterView view = new CounterView();
CounterController controller = new CounterController(model, view); // Controller wires model and view
Scene scene = new Scene(view.getRoot(), 300, 180);
primaryStage.setTitle("MVC Counter App");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clean Code Tip: Strive for thin controllers, fat models In an MVC (or similar MV* patterns like MVP, MVVM) architecture, aim for "thin controllers" and "fat models." Controllers should primarily handle user input and delegate business logic to the Model. The Model, conversely, should contain most of the application's core logic and data manipulation. This makes your business logic more reusable, testable independently of the UI, and easier to manage as the application grows.
Exercise & Solution
Exercise: Refactor the Login Form (GridPane) from Chapter 21 into a basic MVC structure.
- Model: A simple
LoginModelclass that could potentially hold username/password or perform validation (for this exercise, just a dummyisValidLoginmethod). - View: The JavaFX UI (labels, text fields, button).
- Controller: Handles button click, interacts with Model for validation, and updates the View (e.g., displaying a "Login Successful" or "Invalid Credentials" message).
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
// --- Model (to be implemented) ---
class LoginModel {
public boolean isValidLogin(String username, String password) {
// Dummy validation for exercise
return "admin".equals(username) && "password".equals(password);
}
}
// --- View (to be implemented) ---
class LoginView {
private TextField usernameField;
private TextField passwordField;
private Button loginButton;
private Label statusLabel; // To display login status
private VBox root;
public LoginView() {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
grid.add(new Label("User Name:"), 0, 0);
usernameField = new TextField();
usernameField.setPromptText("Enter your username");
grid.add(usernameField, 1, 0);
grid.add(new Label("Password:"), 0, 1);
passwordField = new TextField();
passwordField.setPromptText("Enter your password");
grid.add(passwordField, 1, 1);
loginButton = new Button("Login");
grid.add(loginButton, 1, 2);
statusLabel = new Label(""); // Initially empty
statusLabel.setStyle("-fx-font-size: 14px;");
root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(grid, statusLabel);
}
public VBox getRoot() { return root; }
public TextField getUsernameField() { return usernameField; }
public TextField getPasswordField() { return passwordField; }
public Button getLoginButton() { return loginButton; }
public void setStatusMessage(String message, String color) {
statusLabel.setText(message);
statusLabel.setStyle("-fx-text-fill: " + color + "; -fx-font-size: 14px;");
}
}
// --- Controller (to be implemented) ---
class LoginController {
private LoginModel model;
private LoginView view;
public LoginController(LoginModel model, LoginView view) {
this.model = model;
this.view = view;
attachEventHandlers();
}
private void attachEventHandlers() {
// Your code here: Attach an action to the loginButton
// When clicked, retrieve username/password from view,
// call model.isValidLogin(), and update view.statusLabel accordingly.
}
// Helper method for login logic
private void handleLogin() {
String username = view.getUsernameField().getText();
String password = view.getPasswordField().getText();
if (model.isValidLogin(username, password)) {
view.setStatusMessage("Login Successful!", "green");
System.out.println("Successful login for: " + username);
} else {
view.setStatusMessage("Invalid Credentials!", "red");
System.out.println("Failed login attempt for: " + username);
}
}
}
// --- Main Application Class ---
public class MvcLoginAppExercise extends Application {
@Override
public void start(Stage primaryStage) {
LoginModel model = new LoginModel();
LoginView view = new LoginView();
LoginController controller = new LoginController(model, view);
Scene scene = new Scene(view.getRoot(), 350, 250);
primaryStage.setTitle("MVC Login Form");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Solution:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
// --- Model: Holds the application's data and logic ---
class LoginModel {
public boolean isValidLogin(String username, String password) {
// Simulate a simple validation (e.g., against a hardcoded value)
// In a real application, this would involve database checks, authentication services, etc.
return "admin".equals(username) && "password".equals(password);
}
}
// --- View: Displays the UI and sends user input to the Controller ---
class LoginView {
private TextField usernameField;
private TextField passwordField;
private Button loginButton;
private Label statusLabel;
private VBox root;
public LoginView() {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
grid.add(new Label("User Name:"), 0, 0);
usernameField = new TextField();
usernameField.setPromptText("Enter your username");
grid.add(usernameField, 1, 0);
grid.add(new Label("Password:"), 0, 1);
passwordField = new TextField(); // Use PasswordField in a real app for security
passwordField.setPromptText("Enter your password");
grid.add(passwordField, 1, 1);
loginButton = new Button("Login");
grid.add(loginButton, 1, 2);
statusLabel = new Label("");
statusLabel.setStyle("-fx-font-size: 14px;");
root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(grid, statusLabel);
}
public VBox getRoot() { return root; }
public TextField getUsernameField() { return usernameField; }
public TextField getPasswordField() { return passwordField; }
public Button getLoginButton() { return loginButton; }
public void setStatusMessage(String message, String color) {
statusLabel.setText(message);
statusLabel.setStyle("-fx-text-fill: " + color + "; -fx-font-size: 14px;");
}
}
// --- Controller: Handles user input, updates the Model, and updates the View ---
class LoginController {
private LoginModel model;
private LoginView view;
public LoginController(LoginModel model, LoginView view) {
this.model = model;
this.view = view;
attachEventHandlers();
}
private void attachEventHandlers() {
view.getLoginButton().setOnAction(e -> handleLogin());
}
private void handleLogin() {
String username = view.getUsernameField().getText();
String password = view.getPasswordField().getText();
if (model.isValidLogin(username, password)) {
view.setStatusMessage("Login Successful!", "green");
System.out.println("Successful login for: " + username);
// In a real application, navigate to main app window
} else {
view.setStatusMessage("Invalid Credentials!", "red");
System.out.println("Failed login attempt for: " + username);
}
}
}
// --- Main Application Class ---
public class MvcLoginAppExerciseSolution extends Application {
@Override
public void start(Stage primaryStage) {
LoginModel model = new LoginModel();
LoginView view = new LoginView();
LoginController controller = new LoginController(model, view);
Scene scene = new Scene(view.getRoot(), 350, 250);
primaryStage.setTitle("MVC Login Form");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Chapter 23: Maven Deployment
Quick Theory: Creating an Executable JAR (Fat JAR)
Once you've developed your Java application, whether it's a desktop GUI or a backend service, the next step is to package it for distribution and execution. The standard way to package Java code is into a JAR (Java Archive) file. However, a simple JAR file (jar -cvf MyProgram.jar .) only contains your compiled classes. If your application relies on external libraries (which almost all do, managed by Maven), those libraries won't be included, leading to NoClassDefFoundError at runtime.
A "fat JAR" (also known as an "uber JAR") solves this problem by bundling not only your application's compiled classes but also all its transitive dependencies (all the .jar files listed in your pom.xml's <dependencies>) into a single, self-contained JAR file. This makes deployment incredibly simple: you just distribute one JAR file, and it contains everything needed to run the application, without needing to manually manage a classpath with multiple JARs. The Maven Shade Plugin is commonly used to create these fat JARs.
Professional Code
Let's configure a pom.xml to create an executable fat JAR for our JavaFX application.
Example 1: Basic Maven JAR Plugin for Executable JAR
This shows how to configure maven-jar-plugin to make a regular JAR executable (if it has no external dependencies), by specifying the main class. This won't include dependencies.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.deployment</groupId>
<artifactId>basic-executable-jar</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Maven JAR Plugin: Makes the JAR executable by specifying the main class -->
<!-- This creates a standard JAR, NOT a fat JAR (dependencies are NOT bundled) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<!-- Specify the fully qualified name of your main class -->
<addClasspath>true</addClasspath>
<mainClass>com.example.deployment.BasicApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
To run this (after mvn clean install), you would use java -jar target/basic-executable-jar-1.0-SNAPSHOT.jar.
Note: A dummy BasicApp.java in src/main/java/com/example/deployment/ with a public static void main method is required for this to build.
Example 2: Maven Shade Plugin for a Fat JAR (with JavaFX)
This is the recommended approach for distributing a standalone JavaFX application. It bundles all dependencies, including JavaFX modules, into one JAR. Note: JavaFX modules need to be added as dependencies for a JavaFX app.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.deployment</groupId>
<artifactId>javafx-fat-jar</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>17.0.2</javafx.version> <!-- Use a recent LTS version -->
<main.class>com.example.deployment.FatJarFxApp</main.class>
</properties>
<dependencies>
<!-- JavaFX Core Modules -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- Add other JavaFX modules as needed (e.g., javafx-graphics, javafx-media) -->
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Maven Shade Plugin: Creates a single executable JAR with all dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version> <!-- Use a recent version -->
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<!-- Specify the main class for the executable JAR -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${main.class}</mainClass>
</transformer>
<!-- Handle JavaFX module-info.class merging for fat JAR -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<!-- Optional: Relocate packages to avoid conflicts if needed -->
<!--
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
-->
</configuration>
</execution>
</executions>
</plugin>
<!-- Workaround for JavaFX module system with older JDKs or when creating fat JARs -->
<!-- The javafx-maven-plugin is typically used for modular JavaFX,
but with shade plugin, the mainClass entry in Manifest handles entry point. -->
<!-- For JavaFX 11+, you often need a separate launcher class if your main app
class extends Application directly and your JDK is <= 10.
For JDK 11+, you can directly specify the main Application class.
Here, we assume main.class is your JavaFX Application class. -->
</plugins>
</build>
</project>
To build this (after mvn clean install), it will create target/javafx-fat-jar-1.0-SNAPSHOT.jar. You can then run it with java -jar target/javafx-fat-jar-1.0-SNAPSHOT.jar.
Note: A dummy FatJarFxApp.java that extends javafx.application.Application and has a public static void main method is required.
Clean Code Tip: Automate deployment for consistency and reliability Always automate your build and deployment process using tools like Maven or Gradle. Manual compilation, dependency management, and JAR creation are tedious, error-prone, and inconsistent. Automated builds ensure that your application is always packaged correctly and consistently, which is crucial for reliable delivery to users or production environments.
Exercise & Solution
Exercise: Take any of your previous simple JavaFX applications (e.g., HelloWorldFX from Chapter 19).
- Create a new Maven project for it.
- Add the necessary JavaFX dependencies for
javafx-controls(andjavafx-fxmlif you were using FXML, though not covered in detail here). - Configure the
maven-shade-pluginin yourpom.xmlto create a fat JAR that includes all JavaFX dependencies and sets your application'smainclass as the entry point.
<!-- Your pom.xml structure goes here, adapting the JavaFX fat JAR example -->
Solution:
Let's assume the JavaFX application is named MyJavaFxApp located at src/main/java/com/example/app/MyJavaFxApp.java:
// src/main/java/com/example/app/MyJavaFxApp.java
package com.example.app;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class MyJavaFxApp extends Application {
@Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Label helloLabel = new Label("Hello from Fat JAR JavaFX!");
root.getChildren().add(helloLabel);
Scene scene = new Scene(root, 350, 150);
primaryStage.setTitle("Fat JAR Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
And the pom.xml to build it:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.app</groupId>
<artifactId>my-javafx-fat-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>17.0.2</javafx.version>
<!-- Specify your main JavaFX Application class here -->
<main.class>com.example.app.MyJavaFxApp</main.class>
</properties>
<dependencies>
<!-- JavaFX Modules -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- Add other JavaFX modules if your app uses them (e.g., javafx-graphics, javafx-media) -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Maven Shade Plugin for creating a fat JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- Configure the main class for the executable JAR's manifest -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${main.class}</mainClass>
</transformer>
<!-- Essential for JavaFX fat JARs to correctly merge module-info.class content -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<createDependencyReducedPom>false</createDependencyReducedPom> <!-- Prevent POM generation issues -->
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Chapter 24: Final Junior Checklist
Quick Theory: Reflecting on Your Journey
You've now traversed a comprehensive path, from the absolute fundamentals of Java syntax to advanced concepts in functional programming, data persistence, GUI development, and project deployment. This curriculum was designed to equip you with the essential knowledge and practical skills expected of a competent Junior Java Developer. The journey doesn't end here; software development is a continuous learning process. However, you've built a strong foundation.
This checklist serves as a self-assessment tool. If you can confidently explain and practically apply the concepts listed below, you are well-prepared for entry-level Java development roles and are ready to tackle more complex challenges and specialized frameworks (like Spring Boot) that build upon this foundation. Keep practicing, keep building, and never stop learning!
Final Junior Checklist
-
Book 1: Java Fundamentals
- Core Concepts:
- JDK, JRE, JVM: Understand their roles.
- Variables & Data Types: Primitives (int, double, boolean, char),
String. - Operators: Arithmetic, relational, logical, assignment.
- Control Flow:
if-else,switch,forloops,whileloops,do-whileloops. - Arrays: Declaration, initialization, iteration.
- Object-Oriented Programming (OOP):
- Classes & Objects: Definition, instantiation.
- Constructors: Default, parameterized,
thiskeyword. - Encapsulation: Access modifiers (
public,private,protected), getters/setters. - Inheritance:
extends,superkeyword, method overriding. - Polymorphism: Method overloading, abstract classes, interfaces.
- Abstraction:
abstractkeyword, abstract methods, interfaces vs. abstract classes.
- Collections Framework:
List(e.g.,ArrayList,LinkedList): Basic operations, when to use each.Set(e.g.,HashSet,TreeSet): Basic operations, uniqueness.Map(e.g.,HashMap,TreeMap): Key-value pairs, basic operations.
- Exception Handling:
try-catch-finallyblocks.throw,throwskeywords.- Checked vs. Unchecked exceptions.
- Basic I/O:
System.out.println(),Scanner.- Reading/writing text files (basic
FileReader/FileWriterorFilesutility).
- Core Concepts:
-
Book 2: Advanced Java Concepts
- Generics:
- Type parameters (
<T>). - Generic classes and methods.
- Bounded type parameters (
<T extends Number>). - Wildcards (
? extends,? super).
- Type parameters (
- Concurrency & Multithreading:
Threadclass,Runnableinterface.synchronizedkeyword (methods, blocks).volatilekeyword.- Basic understanding of thread safety issues (race conditions).
ExecutorServiceandCallable(basic usage).
- Networking (Basic):
- Understanding Sockets (
Socket,ServerSocket). - Basic client-server communication.
- Understanding Sockets (
- Design Patterns:
- Singleton pattern.
- Factory pattern.
- Observer pattern (basic understanding).
- Reflection (Basic):
Class.forName(),obj.getClass().- Accessing method/field names.
- Annotations:
- Understanding built-in annotations (
@Override,@Deprecated). - Basic concept of custom annotations.
- Understanding built-in annotations (
- Date & Time API (java.time):
LocalDate,LocalTime,LocalDateTime,Instant.Duration,Period.- Formatting and parsing (
DateTimeFormatter).
- Generics:
-
Book 3: Functional Revolution & Data Persistence
- Functional Programming (Java 8+):
- Lambda Expressions: Syntax
(params) -> { body }, usage with functional interfaces (Predicate,Consumer,Function,Supplier). - Stream API:
- Intermediate Operations:
filter(),map(),sorted(). - Terminal Operations:
collect(Collectors.toList()),count(),forEach(),reduce(),min()/max().
- Intermediate Operations:
OptionalClass: PreventingNullPointerException(ofNullable,isPresent,orElse,map,ifPresent).- Method References:
ClassName::methodName,objectName::methodName,ClassName::new.
- Lambda Expressions: Syntax
- Build Automation (Maven):
pom.xmlstructure:groupId,artifactId,version,properties.- Dependency Management: Adding
<dependency>entries. - Build Lifecycle:
clean,compile,test,package,install.
- JDBC (Java Database Connectivity):
- Fundamentals: Driver loading,
Connection,Statement,ResultSet. try-with-resourcesfor resource management.- CRUD Operations:
INSERT,SELECT,UPDATE,DELETEvia JDBC. PreparedStatement: Mandatory for SQL injection prevention (?placeholders).- Transaction Management:
setAutoCommit(false),commit(),rollback().
- Fundamentals: Driver loading,
- DAO Pattern:
- Purpose: Separate persistence logic from business logic.
- Structure: Model, DAO Interface, DAO Implementation.
- Desktop GUIs (JavaFX):
- Basic Architecture:
Stage,Scene,Nodehierarchy. - UI Components:
Label,Button,TextField(basic usage). - Event Handling:
setOnActionwith lambdas. - Layout Managers:
VBox,HBox,GridPane(responsive UI design). - MVC Pattern: Basic application of Model-View-Controller for GUIs.
- Basic Architecture:
- Deployment:
- Maven Shade Plugin: Creating executable "fat JARs" for standalone distribution.
- Functional Programming (Java 8+):
Clean Code Tip: Continuous learning and practice are key The journey of a software developer is one of continuous learning. The concepts covered in these books provide a strong foundation, but the landscape of technology is always evolving. Stay curious, experiment with new tools and frameworks, read documentation, contribute to open-source projects, and consistently build things. Practice is the most effective way to solidify your understanding and grow your skills.
END OF BOOK 3: FINAL PART
You have successfully completed the qualification path. Your dedication to learning and mastering these topics is commendable. You are now equipped with the knowledge and foundational skills to embark on your career as a Junior Java Developer. Good luck on your next steps!