Java

Introduction To Java

wwww

  1. Complete Java Programming Learning Roadmap
    1. Chapter 1: Introduction to Java — Why Java Still Matters in 2026
      1. 1.1 History of Java
      2. 1.2 Important Facts About Java
      3. 1.3 Features of Java
      4. 1.4 Java vs Other Languages
      5. 1.5 Object-Oriented Programming (OOP) Concepts
      6. 1.6 Software Development Process in Java
      7. 1.7 Career Relevance of Java
      8. 1.8 Dependencies and Prerequisites
      9. 1.9 Common Beginner Mistakes
      10. 1.10 Role of AI in Java Development
    2. Chapter 2: Detailed Setup and First Application
      1. 2.1 Installing Java (JDK)
        1. Windows Setup
        2. macOS Setup
        3. Linux (Ubuntu) Setup
      2. 2.2 Installing an IDE (Optional but Recommended)
      3. 2.3 Structure of a Java Program
      4. 2.4 Compiling and Running Your First Program
      5. 2.5 Compilation and Execution Process
      6. 2.6 Tips for Beginners
    3. Chapter 3: Variables, Data Types & Operators
      1. 3.1 Identifiers & Keywords
      2. 3.2 Constants & Literals
      3. 3.3 Variables
      4. 3.4 Operators
        1. Arithmetic Operators (+, -, *, /, %)
        2. Relational Operators (==, !=, <, >, <=, >=)
        3. Logical Operators (&&, ||, !)
        4. Assignment Operators (=, +=, -=, *=, /=, %=)
        5. Unary Operators (++, –, +, -)
        6. Ternary Operator (?:)
        7. Bitwise Operators (&, |, ^, ~, <<, >>, >>>)
      5. 3.5 Type Casting & Type Conversion
      6. 3.6 Comments
    4. Chapter 4: Control Structures
      1. 4.1 Conditional Statements
        1. if, if-else, if-else-if
        2. switch
      2. 4.2 Loops
        1. for Loop
        2. while Loop
        3. do-while Loop
        4. Enhanced for Loop (for-each)
      3. 4.3 Loop Control Statements
        1. break
        2. continue
        3. return
    5. Chapter 5: Functions / Methods
      1. 5.1 Defining Methods
      2. 5.2 Method Components
        1. Access Modifiers (public, private, protected, default)
        2. Return Type and void
        3. Parameters
      3. 5.3 Passing Parameters
        1. Pass-by-Value
        2. Varargs
      4. 5.4 Method Overloading
      5. 5.5 Recursion
      6. 5.6 Static Methods
    6. Chapter 6: Object-Oriented Programming (OOP)
      1. 6.1 Fundamentals
        1. Encapsulation, Inheritance, Polymorphism, Abstraction
      2. 6.2 Classes & Objects
      3. 6.3 Constructors
        1. Default Constructor
        2. Parameterized Constructor
        3. Copy Constructor
        4. Constructor Chaining (this() and super())
      4. 6.4 Static Members
        1. Static Variables
        2. Static Methods
        3. Static Blocks
      5. 6.5 Inheritance
        1. extends
        2. super
        3. Method Overriding
      6. 6.6 Polymorphism
        1. Compile-time Polymorphism (Overloading)
        2. Runtime Polymorphism (Overriding)
      7. 6.7 Encapsulation & Abstraction
        1. Access Modifiers
        2. Getters and Setters
        3. Abstract Classes and Interfaces
      8. 6.8 Interfaces & Abstract Classes
        1. Interfaces
        2. Abstract Classes
      9. 6.9 Advanced OOP Concepts
        1. Inner Classes
        2. Static Nested Classes
        3. Anonymous Classes
        4. instanceof
        5. Packages and Import
    7. Chapter 7: Exception Handling
      1. 7.1 Basics
        1. try, catch, finally
        2. try-with-resources
      2. 7.2 Throwing Exceptions
        1. throw
        2. throws
      3. 7.3 Custom Exceptions
      4. 7.4 Checked vs Unchecked Exceptions
      5. 7.5 Exception Propagation
    8. Chapter 8: Collections & Generics
      1. 8.1 Collection Framework Overview
        1. List, Set, Queue, Map
      2. 8.2 Interfaces
        1. List (ArrayList, LinkedList)
        2. Set (HashSet, TreeSet, LinkedHashSet)
        3. Map (HashMap, TreeMap, LinkedHashMap)
      3. 8.3 Key Implementation Classes
        1. ArrayList vs LinkedList
        2. HashMap vs TreeMap
      4. 8.4 Iterators & Loops
        1. Iterator
        2. ListIterator
        3. Enhanced For-Loop
        4. forEach() Method
      5. 8.5 Generics
        1. Generic Classes
        2. Generic Methods
        3. Wildcards
    9. Chapter 9: File Handling & I/O
      1. 9.1 Text Streams
        1. FileReader and BufferedReader
        2. FileWriter and BufferedWriter
      2. 9.2 Binary Streams
        1. FileInputStream and FileOutputStream
        2. DataInputStream and DataOutputStream
      3. 9.3 Object Serialization
        1. Serializable Interface
        2. Transient and Static Fields
    10. Chapter 10: Multithreading & Concurrency
      1. 10.1 Thread Creation
        1. Thread Class
        2. Runnable Interface
        3. Callable Interface
      2. 10.2 Thread Life Cycle & Methods
        1. Thread States
        2. sleep()
        3. join()
        4. interrupt()
      3. 10.3 Synchronization & Locks
        1. Synchronized Methods
        2. Synchronized Blocks
        3. ReentrantLock
      4. 10.4 Thread Safety & Race Conditions
        1. Race Conditions
        2. volatile
        3. Atomic Variables
      5. 10.5 Executor Framework & Thread Pools
        1. ExecutorService
        2. FixedThreadPool
        3. CachedThreadPool
        4. ScheduledThreadPool
        5. Future and Callable
    11. Chapter 11: Networking & JDBC
      1. 11.1 Networking Basics
        1. TCP vs UDP
        2. InetAddress
      2. 11.2 Sockets: Server-Client Communication
        1. ServerSocket
        2. Socket (Client)
      3. 11.3 JDBC: Connecting to Database
        1. DriverManager and Connection
        2. Statement, PreparedStatement, CallableStatement
      4. 11.4 CRUD Operations
        1. Create (INSERT)
        2. Read (SELECT)
        3. Update (UPDATE)
        4. Delete (DELETE)
    12. Chapter 12: Advanced Java
      1. 12.1 Lambda Expressions & Functional Interfaces
        1. Lambda Expressions
        2. Functional Interfaces
      2. 12.2 Streams API & Parallel Streams
        1. Intermediate Operations
        2. Terminal Operations
        3. Parallel Streams
      3. 12.3 Java 8+ Features
        1. Date-Time API
        2. Optional
        3. Method References
        4. CompletableFuture
      4. 12.4 Annotations & Reflection
        1. Annotations
        2. Reflection
    13. Chapter 13: Practical Implementation
      1. 13.1 Real-World Projects
        1. Banking System
        2. Library Management System
  2. Final Advice

Complete Java Programming Learning Roadmap

Chapter 1: Introduction to Java — Why Java Still Matters in 2026

When you open your laptop right now, you’re interacting with code written in Java. Your Android phone runs applications built in Java. The banking system that processes your transactions relies on Java. The enterprise systems that power Fortune 500 companies are built with Java. Despite being over three decades old, Java remains one of the most influential and widely-used programming languages in the world.

Java has a reputation for being “verbose.” It’s not—it’s just explicit, and this is a very different thing. Python and JavaScript let you get away with sloppy habits because they dynamically manage types and memory. Java refuses to do that. It forces you to actually understand what your code is doing at every step—what type each variable holds, what exceptions might be thrown, and how objects interact. This explicitness is precisely why Java still powers enterprise systems, Android applications, and large-scale distributed systems decades after “easier” languages appeared. It’s also exactly why learning it properly makes you a noticeably stronger programmer in every other language you touch afterward.

The goal of this guide is comprehensive: to take you from complete beginner to confident, job-ready Java programmer, on Windows, Mac, or Linux. We provide plain-English explanations first, followed by a full professional-grade roadmap covering every stage from your very first “Hello, World!” to enterprise application development and career readiness.

This comprehensive introduction covers everything you need to know before diving into actual coding. We explore what Java actually is and why it was created, the complete setup process for every major operating system, a detailed stage-by-stage roadmap of everything you’ll learn, how to use AI effectively without becoming dependent on it, what happens when you compile and run Java code through the JVM lifecycle, and common pitfalls and how to avoid them. By the end of this guide, you’ll have a crystal-clear understanding of Java and a complete learning path that will take you from absolute beginner to professional developer.

1.1 History of Java

Java was created by James Gosling and his team at Sun Microsystems in the early 1990s. The project, originally called “Oak,” was started in 1991 and renamed to Java in 1995. The language was designed with a simple yet powerful philosophy: “Write Once, Run Anywhere” (WORA).

The motivation behind Java was simple: existing languages like C and C++ were powerful but platform-dependent and had memory management issues. Gosling wanted a language that could run on any device—from set-top boxes to computers—without recompilation. This vision led to the creation of the Java Virtual Machine (JVM), which makes Java platform-independent.

The key idea of Java was portability and simplicity. By compiling code to bytecode instead of machine code, Java programs could run on any system with a JVM. This was revolutionary at the time and made Java the language of choice for web applications, enterprise software, and eventually, Android development.

The Evolution of Java

VersionYearKey Features
JDK 1.01996The first public release
JDK 1.11997Inner classes, JDBC, RMI
J2SE 1.21998Collections Framework, Swing
J2SE 1.32000Java Naming Directory Interface (JNDI)
J2SE 1.42002Regular expressions, NIO
J2SE 5.02004Generics, enhanced for loop, annotations
Java SE 62006Scripting, JDBC 4.0
Java SE 72011Try-with-resources, NIO.2, Fork/Join
Java SE 82014Lambda expressions, Streams API, Date/Time API
Java SE 92017Module system (Project Jigsaw)
Java SE 102018Local-variable type inference (var)
Java SE 112018LTS release, new HTTP Client
Java SE 172021LTS release, sealed classes, pattern matching
Java SE 212023LTS release, virtual threads, pattern matching

Each version adds safer, more expressive ways to write code—lambdas for functional programming, modules for better encapsulation, and virtual threads for massive scalability—without breaking the huge amount of existing Java code already running in production systems worldwide. This backward-compatibility promise is a big reason companies trust Java for decades-long projects. Code written in 1996 can still run on a modern Java 21 JVM. This stability is unusual in the programming world and has kept Java relevant across generations of developers.

1.2 Important Facts About Java

Java is a compiled and interpreted language. Source code (.java) is compiled to bytecode (.class), which is then interpreted and executed by the Java Virtual Machine (JVM). This two-step process enables platform independence.

Java is platform-independent. The bytecode runs on any JVM, regardless of the underlying operating system. This is the foundation of “Write Once, Run Anywhere.”

Java is object-oriented.In Java, most values other than primitive data types are represented as objects. This object-oriented approach encourages reusable components and supports a modular program structure.

Java is strongly typed. Every variable must be declared with a specific type, and type checking is enforced by the compiler.

Java is robust. Features like exception handling, garbage collection, and type safety make Java programs more reliable.

Java is secure. Built-in security features protect against malware and unauthorized access.

Java has automatic memory management. The garbage collector automatically handles memory allocation and deallocation, freeing developers from manual memory management.

Java is multithreaded. Built-in support for concurrent programming allows efficient use of multi-core processors.

Java is high-performance. Just-In-Time (JIT) compilation optimizes bytecode to native machine code for improved performance.

Java is dynamic. Supports dynamic class loading and reflection, enabling flexible and extensible applications.

Java is distributed. Built-in networking capabilities make it easy to build distributed applications.

1.3 Features of Java

FeatureDescription
Platform IndependenceBytecode runs on any JVM
Object-OrientedSupports OOP principles (encapsulation, inheritance, polymorphism, abstraction)
RobustStrong memory management and exception handling
SecureSecurity features built into the language and runtime
MultithreadedBuilt-in support for concurrent programming
High PerformanceJust-In-Time (JIT) compilation optimizes code
DynamicSupports dynamic class loading and reflection
DistributedBuilt-in networking capabilities
Automatic Garbage CollectionMemory management is handled automatically
Rich APIExtensive standard library for common tasks

1.4 Java vs Other Languages

Java vs C

AspectJavaC
Memory ManagementAutomatic garbage collectionManual (malloc/free)
PlatformPlatform-independentPlatform-dependent
PointersNo direct pointersFull pointer support
CompilationCompiles to bytecodeCompiles to machine code
DifficultyEasier to learnMore complex
Object-OrientedFully object-orientedProcedural
SecurityBuilt-in security featuresNo built-in security
PerformanceGood (JIT compiled)Excellent

Java vs C++

AspectJavaC++
Memory ManagementAutomatic garbage collectionManual memory management
Multiple InheritanceThrough interfacesFull multiple inheritance
PointersNo direct pointersFull pointer support
Operator OverloadingNot supportedSupported
ComplexitySimplerMore complex
PlatformPlatform-independentPlatform-dependent
CompilationCompiles to bytecodeCompiles to machine code
PerformanceGoodExcellent

Java vs Python

AspectJavaPython
TypingStatically-typedDynamically-typed
PerformanceFaster (JIT compiled)Slower (interpreted)
SyntaxMore verboseConcise and readable
EcosystemEnterprise, Android, Big DataData Science, Scripting, Web
Learning CurveModerateEasier
PlatformJVM (cross-platform)Cross-platform
CommunityMature, enterprise focusRapidly growing, diverse

1.5 Object-Oriented Programming (OOP) Concepts

Java is fundamentally object-oriented. The four pillars of OOP are:

Encapsulation means keeping an object’s internal details private, exposing only what’s needed. This promotes modularity and protects data integrity. In Java, encapsulation is achieved through access modifiers (private, protected, public, default). By hiding implementation details, you can change internal workings without affecting code that uses the class.

Inheritance means a new class can inherit properties and behaviors from an existing class. This promotes code reuse and establishes relationships between classes. In Java, inheritance is achieved using the extends keyword. Subclasses can override methods from the parent class and add new functionality while reusing existing code.

Polymorphism means the same action, called the same way, can behave differently depending on the object it’s called on. This makes code more flexible and extensible. In Java, polymorphism is achieved through method overloading (compile-time) and method overriding (runtime). Polymorphism allows you to treat objects of different types as objects of a common superclass.

Abstraction means showing only essential features and hiding unnecessary complexity. This simplifies the interface and reduces complexity. In Java, abstraction is achieved through abstract classes and interfaces. By focusing on what an object does rather than how it does it, you can create clean, maintainable code.

1.6 Software Development Process in Java

The Java development workflow follows these general steps:

  1. Edit source code (in .java files)
  2. Compile with javac compiler to produce bytecode (.class files)
  3. Load bytecode into the Java Virtual Machine (JVM)
  4. Verify bytecode for security and correctness
  5. Execute the program on the JVM
  6. Analyze output and results

Complete Java Execution Lifecycle:

Source Code (.java) → Compiler (javac) → Bytecode (.class) → Class Loader → Bytecode Verifier → JVM Execution → Output

The JVM provides:

  • Class Loader: Loads bytecode files into memory
  • Bytecode Verifier: Checks bytecode for security violations
  • Just-In-Time (JIT) Compiler: Optimizes bytecode to native machine code
  • Garbage Collector: Automatically manages memory
  • Thread Scheduler: Manages concurrent threads

1.7 Career Relevance of Java

Java skills remain highly valuable in several roles:

RoleWhy Java Matters Here
Backend DeveloperBuilds enterprise applications using Spring, Hibernate
Android DeveloperBuilds Android applications using Java
Big Data EngineerWorks with Hadoop, Spark, Kafka
Microservices DeveloperBuilds distributed systems using Spring Boot
DevOps EngineerAutomates deployments using Java tools
Full Stack DeveloperBuilds complete applications with Java backends

Why Companies Still Use Java:

  • Large ecosystem of libraries and frameworks
  • Mature tools for development, testing, and deployment
  • Strong community with extensive documentation
  • Enterprise support from vendors like Oracle
  • Stable and reliable for mission-critical applications
  • Scalable from small applications to massive distributed systems
  • Excellent job market with high demand for Java developers
  • Long-term viability with regular updates and LTS releases

1.8 Dependencies and Prerequisites

You Do NOT Need:

  • A math or computer science degree.
  • Prior programming experience.
  • An expensive or powerful computer.

You DO Need:

  • Patience with compiler errors. Java compiler errors can look intimidating, especially for beginners. This gets dramatically easier with practice.
  • A willingness to understand why an error happened.
  • Comfort navigating files and folders and using a terminal.
  • A modern computer (anything from the last decade will work).

1.9 Common Beginner Mistakes

MistakeDescription
Forgetting the main signaturepublic static void main(String[] args) is required
Mismatched data typesAssigning incompatible types (e.g., int to String)
Off-by-one errorsArray indexing errors
Not closing resourcesFiles, connections, streams
Misplaced semicolonsPutting semicolons where they don’t belong
Using ‘==’ for string comparisonShould use .equals() instead
Forgetting to import classesJava requires explicit imports for most classes
NullPointerExceptionDereferencing a null reference

1.10 Role of AI in Java Development

AI assistants like ChatGPT, Claude, and GitHub Copilot are especially valuable for Java because the language has a large ecosystem and many patterns to learn.

Good Uses of AI:

  • “Explain this compiler error in plain English.”
  • “How do I implement this design pattern in Java?”
  • “Convert this Python code to Java.”
  • “Show me how to use Streams API with this collection.”
  • “What is the most appropriate way to handle this exception?”

Habits to Avoid:

  • Pasting AI-generated code without understanding it.
  • Trusting AI blindly about Java’s behavior.
  • Skipping the “why” behind a fix.

Chapter 2: Detailed Setup and First Application

Every Java setup, regardless of operating system, needs exactly two things:

  1. The Java Development Kit (JDK): The tools to compile and run Java code.
  2. An editor or IDE: Where you actually write your code.

Recommended Approach: Oracle OpenJDK or Eclipse Adoptium (formerly AdoptOpenJDK). Both are free, well-maintained, and reliable.

2.1 Installing Java (JDK)

Windows Setup

  1. Download the JDK from https://adoptium.net/.
  2. Run the installer and follow the default installation steps. The default installation location is C:\Program Files\Eclipse Adoptium\jdk-xx.x.x.
  3. Add the JDK to your PATH:
    • Open the Start Menu and use the search bar to look for “Environment Variables.”
    • Click “Edit the system environment variables.”
    • Click “Environment Variables…”
    • Under “System variables,” find the Path variable and click “Edit…”
    • Click “New” and add the bin folder (usually C:\Program Files\Eclipse Adoptium\jdk-xx.x.x\bin).
    • Click “OK” on all windows.
  4. Verify Installation:
    Open Command Prompt and run:java -version javac -versionYou should see version information for both.

macOS Setup

  1. Install using Homebrew:brew install openjdk
  2. Verify Installation:java -version javac -version

Linux (Ubuntu) Setup

  1. Install using apt:sudo apt update sudo apt install openjdk-17-jdk
  2. Verify Installation:java -version javac -version

IntelliJ IDEA Community Edition (Recommended)

Why I recommend this: IntelliJ IDEA is widely considered the best Java IDE. The Community Edition is free and provides excellent features for Java development including smart autocomplete, refactoring tools, and debugging support.

  1. Download from https://www.jetbrains.com/idea/download/.
  2. Run the installer and follow the default steps.
  3. Create a new project and select Java.

VS Code

Why I recommend this: Lightweight, free, and works well for Java with the right extensions. Good for those who want a simpler editor.

  1. Download from https://code.visualstudio.com/.
  2. Open VS Code and select the Extensions panel using the shortcut Ctrl + Shift + X.
  3. Search for and install the “Java Extension Pack” by Microsoft.

Eclipse IDE

Why I recommend this: Free, mature, and widely used in enterprise settings. Good for those who prefer a traditional IDE.

  1. Download from https://www.eclipse.org/downloads/.
  2. Run the installer and select “Eclipse IDE for Java Developers.”

2.3 Structure of a Java Program

Every Java program has a specific structure that must be followed. Understanding each element is crucial before writing more complex code.

The basic structure of a Java program includes:

  • Package declaration: (optional) Organizes classes into namespaces
  • Import statements: (optional) Brings in other classes
  • Class declaration: The main container for code
  • Fields: Variables that belong to the class
  • Methods: Functions that belong to the class
  • Main method: The entry point of the program

Creating Your First Project:

  1. Create a project folder:mkdir java-course cd java-course
  2. Create a new file named Hello.java.
  3. Paste this code:
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Breaking Down the Code:

ElementPurpose
public class HelloDefines the class named “Hello”
public static void main(String[] args)The main method—entry point of the program
System.out.println()Prints output to the console

Note: The filename (Hello.java) must exactly match the class name (Hello). Java is case-sensitive, so Hello.java and hello.java are different files.

2.4 Compiling and Running Your First Program

Compiling and Running:

  1. Open a terminal (Command Prompt, Terminal, or bash).
  2. Navigate to your project folder:cd java-course
  3. Compile:javac Hello.javaThis creates a file called Hello.class in your project folder.
  4. Run:java HelloNote: Do not add .class when running—use the class name only.

You should see:

Hello, World!

Common Problems:

ProblemFix
'javac' is not recognizedThe JDK PATH wasn’t set correctly. Double-check the PATH step.
error: Class names are only acceptedYou’re trying to run java Hello.class instead of java Hello.
Hello.java:1: error: class Hello is publicThe filename doesn’t match the class name.

2.5 Compilation and Execution Process

Understanding what happens behind the scenes removes confusion later.

Stage 1: Compilation
The javac compiler reads your .java file and produces bytecode in .class files. This is where syntax errors are caught—if your code has mistakes, the compiler will tell you here.

Stage 2: Class Loading
The Class Loader loads the .class file into the JVM memory. The JVM finds the class, verifies it’s valid, and prepares it for execution.

Stage 3: Bytecode Verification
The Bytecode Verifier checks for security violations and invalid code. This prevents malicious code from causing harm.

Stage 4: Execution
The JVM executes the bytecode. The Just-In-Time (JIT) compiler may optimize bytecode to native machine code for frequently executed sections, improving performance.

Why This Pipeline Matters

Knowing this pipeline is what makes error categories make sense later. When you see a “class not found” error, you know it’s a classpath issue. When you see a “cannot find symbol” error, you know it’s a compilation issue. This helps you narrow down where to look for the problem.

2.6 Tips for Beginners

Recommended Learning Approach:

  • Begin by learning the fundamentals, including variables, data types, and control flow, to build a strong foundation in programming.
  • Practice daily: Write code regularly, even if it’s small programs.
  • Read error messages carefully: They often tell you exactly what’s wrong.
  • Use the Java API documentation: The official documentation is your best friend.
  • Break problems into smaller pieces: Solve one small part at a time.
  • Write clean code: Use meaningful variable names and comments.
  • Learn to use your IDE: Use shortcuts and features to work more efficiently.
  • Don’t memorize everything: Focus on understanding concepts and knowing where to look up syntax.

Common Beginner Mistakes to Avoid:

  • Ignoring error messages can make debugging harder because they often provide important clues about what went wrong and where the problem occurred.
  • Trying to learn everything at once: Master fundamentals before moving to advanced topics.
  • Copy-pasting code without understanding: Type the code yourself to learn.
  • Not practicing enough: Programming is a skill that requires practice.
  • Giving up too quickly: Everyone struggles at first—persistence pays off.

Chapter 3: Variables, Data Types & Operators

3.1 Identifiers & Keywords

Identifiers are programmer-defined names used to distinguish elements such as classes, methods, variables, and other components within a program. Keywords are reserved words with special meaning in Java.

Identifiers must start with a letter, underscore (_), or dollar sign ($). After the first character, they can contain letters, digits, underscores, and dollar signs. Java is case-sensitive, so bird and Bird are different. Naming conventions include camelCase for methods and variables, PascalCase for classes, and ALL_CAPS for constants. Keywords like public, class, static, void, int, if, else, for, and while cannot be used as identifiers.

Code Example

public class Bird {
    // Valid identifiers
    int sparrowCount = 10;
    String eagleName = "Eagle";
    final int MAX_BIRDS = 100;
    
    // Invalid identifiers (commented out)
    // int 1bird = 5;        // ERROR: Cannot start with digit
    // int bird-count = 3;   // ERROR: Hyphen not allowed
    // int class = 5;        // ERROR: 'class' is a keyword
    
    public static void main(String[] args) {
        Bird bird = new Bird();
        System.out.println("Sparrow count: " + bird.sparrowCount);
        System.out.println("Max birds: " + MAX_BIRDS);
    }
}

Identifiers are case-sensitive. Keywords are reserved and cannot be used as names. Following naming conventions improves code readability.

3.2 Constants & Literals

Constants are variables whose values cannot change after assignment. Literals are fixed values directly written in code.

Literals include integer literals (like 10, 0b1010), floating-point literals (3.14, 2.5f), character literals ('A'), boolean literals (true, false), and string literals ("Sparrow"). Escape sequences like \n (newline), \t (tab), and \" (double quote) represent special characters. final variables can be assigned a value only once, preventing that variable from being reassigned afterward.

Code Example

public class ConstantsLiterals {
    public static void main(String[] args) {
        // Constants
        final int MAX_BIRDS = 100;
        final String SPECIES = "Sparrow";
        
        // Literals
        int count = 10;              // Integer literal
        double weight = 25.5;        // Floating-point literal
        char color = 'R';            // Character literal
        boolean isMigrating = true;  // Boolean literal
        String name = "Eagle";       // String literal
        
        // Escape sequences
        String text = "Line1\nLine2\tTabbed\"Quote\"";
        System.out.println(text);
        
        System.out.println("Species: " + SPECIES);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight + " grams");
        
        // Uncommenting causes error:
        // MAX_BIRDS = 200;  // ERROR: Cannot reassign final variable
    }
}

Literals are fixed values. The final keyword creates constants. Escape sequences represent special characters in strings.

3.3 Variables

Variables are named memory locations used to store values of a particular data type while a program is running.

Local variables are declared inside methods and exist only within that method. Instance variables belong to objects and exist as long as the object exists. Static variables belong to the class and are shared among all instances. Default values for instance/static variables are 0 for numbers, false for boolean, and null for objects.

Code Example

public class Bird {
    // Instance variables (belong to objects)
    String species;
    int count;
    
    // Static variable (belongs to class)
    static int totalBirds = 0;
    
    // Constructor
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
        totalBirds += count;
    }
    
    public void display() {
        // Local variable (exists only in this method)
        String message = "Bird Info";
        System.out.println(message);
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Total birds: " + totalBirds);
    }
    
    public static void main(String[] args) {
        Bird sparrow = new Bird("Sparrow", 10);
        Bird eagle = new Bird("Eagle", 3);
        
        sparrow.display();
        System.out.println();
        eagle.display();
    }
}

Instance variables are unique per object. Static variables are shared. Local variables exist only in their method. The this keyword refers to the current object.

3.4 Operators

Arithmetic Operators (+, -, *, /, %)

Arithmetic operators perform mathematical calculations on numeric values.

+ addition, - subtraction, * multiplication, / division, % modulo (remainder). Integer division truncates the result. Floating-point division returns a decimal result.

Code Example

public class ArithmeticOperators {
    public static void main(String[] args) {
        int eagles = 10;
        int hawks = 3;
        int sparrows = 5;
        
        // Basic arithmetic
        int sum = eagles + hawks + sparrows;
        int diff = eagles - hawks;
        int product = eagles * hawks;
        int quotient = eagles / hawks;  // Integer division: 10/3 = 3
        int remainder = eagles % hawks; // Modulo: 10%3 = 1
        
        System.out.println("Eagles: " + eagles);
        System.out.println("Hawks: " + hawks);
        System.out.println("Sparrows: " + sparrows);
        System.out.println("Sum: " + sum);
        System.out.println("Difference: " + diff);
        System.out.println("Product: " + product);
        System.out.println("Quotient: " + quotient);
        System.out.println("Remainder: " + remainder);
        
        // Floating-point division
        double doubleQuotient = (double) eagles / hawks;
        System.out.println("Floating quotient: " + doubleQuotient);
    }
}

Integer division truncates. Modulo returns the remainder. Casting to double enables floating-point division.

Relational Operators (==, !=, <, >, <=, >=)

Relational operators compare values and return boolean results.

== equal to, != not equal to, < less than, > greater than, <= less than or equal, >= greater than or equal. For objects, == compares references, .equals() compares content.

Code Example

public class RelationalOperators {
    public static void main(String[] args) {
        int birds = 10;
        int eagles = 3;
        int hawks = 3;
        
        // Integer comparisons
        System.out.println("birds == eagles: " + (birds == eagles));
        System.out.println("birds != eagles: " + (birds != eagles));
        System.out.println("birds > eagles: " + (birds > eagles));
        System.out.println("eagles < birds: " + (eagles < birds));
        System.out.println("eagles >= hawks: " + (eagles >= hawks));
        System.out.println("eagles <= hawks: " + (eagles <= hawks));
        
        // String comparisons
        String bird1 = "Sparrow";
        String bird2 = "Sparrow";
        String bird3 = new String("Sparrow");
        
        System.out.println("\nString comparisons:");
        System.out.println("bird1 == bird2: " + (bird1 == bird2));  // true
        System.out.println("bird1 == bird3: " + (bird1 == bird3));  // false
        System.out.println("bird1.equals(bird3): " + bird1.equals(bird3)); // true
    }
}

Relational operators produce boolean results. For objects, use .equals() for content comparison.

Logical Operators (&&, ||, !)

Logical operators combine boolean expressions to produce boolean results.

&& (AND) returns true only if both operands are true. || (OR) returns true if at least one operand is true. ! (NOT) negates a boolean value. Short-circuit evaluation stops evaluating when the result is determined.

Code Example

public class LogicalOperators {
    public static void main(String[] args) {
        boolean hasEagles = true;
        boolean hasHawks = false;
        boolean hasSparrows = true;
        
        // AND (both must be true)
        System.out.println("hasEagles && hasSparrows: " + 
                          (hasEagles && hasSparrows)); // true
        System.out.println("hasEagles && hasHawks: " + 
                          (hasEagles && hasHawks)); // false
        
        // OR (at least one must be true)
        System.out.println("hasEagles || hasHawks: " + 
                          (hasEagles || hasHawks)); // true
        System.out.println("hasHawks || hasSparrows: " + 
                          (hasHawks || hasSparrows)); // true
        
        // NOT (negation)
        System.out.println("!hasHawks: " + !hasHawks); // true
        
        // Short-circuit example
        int birds = 10;
        if (birds > 5 && birds < 20) {
            System.out.println("Birds in range: " + birds);
        }
        
        // Short-circuit stops evaluation early
        int count = 0;
        if (count > 0 && 10 / count > 1) {
            System.out.println("Won't execute (short-circuit)");
        }
    }
}

&& stops at first false. || stops at first true. ! negates the boolean value.

Assignment Operators (=, +=, -=, *=, /=, %=)

Assignment operators assign values to variables. Compound assignment combines operation and assignment.

The assignment operators provide different ways to update a variable: = assigns a value directly, while +=, -=, *=, /=, and %= perform the specified arithmetic operation and store the resulting value back in the variable.

Code Example

public class AssignmentOperators {
    public static void main(String[] args) {
        int birds = 10;
        
        System.out.println("Initial: " + birds);
        
        // Simple assignment
        birds = 20;
        System.out.println("After =: " + birds);
        
        // Compound assignment
        birds += 5;   // birds = birds + 5
        System.out.println("After +=: " + birds);
        
        birds -= 3;   // birds = birds - 3
        System.out.println("After -=: " + birds);
        
        birds *= 2;   // birds = birds * 2
        System.out.println("After *=: " + birds);
        
        birds /= 4;   // birds = birds / 4
        System.out.println("After /=: " + birds);
        
        birds %= 3;   // birds = birds % 3
        System.out.println("After %=: " + birds);
        
        // Chained assignment
        int eagles = birds;  // Both eagles and birds have same value
        System.out.println("Chained: eagles = " + eagles);
    }
}

Compound operators perform operation and assignment in one step. Chained assignment allows a single value to be assigned to several variables within the same statement.

Unary Operators (++, –, +, -)

Unary operators operate on a single operand.

The unary operators ++ and -- increase or decrease a variable by one, while unary + preserves a value’s sign and unary - reverses it. Prefix (++x) increments then uses value. Postfix (x++) uses value then increments.

Code Example

public class UnaryOperators {
    public static void main(String[] args) {
        int eagles = 5;
        int hawks = 5;
        
        // Prefix increment (increment then use)
        System.out.println("Prefix increment:");
        System.out.println("Before: eagles = " + eagles);
        System.out.println("++eagles: " + (++eagles));  // 6
        System.out.println("After: eagles = " + eagles); // 6
        
        // Postfix increment (use then increment)
        System.out.println("\nPostfix increment:");
        System.out.println("Before: hawks = " + hawks);
        System.out.println("hawks++: " + (hawks++));    // 5
        System.out.println("After: hawks = " + hawks);   // 6
        
        // Unary minus
        int positive = 10;
        int negative = -positive;
        System.out.println("\nUnary minus:");
        System.out.println("positive: " + positive);
        System.out.println("-positive: " + negative);
    }
}

Prefix operators modify before use. Postfix operators use before modifying. Unary minus negates a value.

Ternary Operator (?:)

The ternary operator provides a shorthand for simple if-else statements.

Syntax: condition ? value_if_true : value_if_false. Returns one of two values based on condition. Can be nested but readability suffers.

Code Example

public class TernaryOperator {
    public static void main(String[] args) {
        int birds = 10;
        
        // Simple ternary
        String status = (birds > 5) ? "Many birds" : "Few birds";
        System.out.println("Status: " + status);
        
        // Using ternary for assignment
        int count = (birds > 0) ? birds : 0;
        System.out.println("Count: " + count);
        
        // Ternary with calculation
        int doubled = (birds > 5) ? birds * 2 : birds;
        System.out.println("Doubled if > 5: " + doubled);
        
        // Nested ternary (avoid for readability)
        String category = (birds > 20) ? "Large" : 
                         (birds > 10) ? "Medium" : "Small";
        System.out.println("Category: " + category);
        
        // Equivalent if-else
        if (birds > 20) {
            category = "Large";
        } else if (birds > 10) {
            category = "Medium";
        } else {
            category = "Small";
        }
        System.out.println("Category (if-else): " + category);
    }
}

The condition is evaluated. When the condition evaluates to true, the first expression provides the resulting value. If false, the second is returned.

Bitwise Operators (&, |, ^, ~, <<, >>, >>>)

Bitwise operators manipulate individual bits of integer values.

& AND, | OR, ^ XOR, ~ NOT, << left shift, >> right shift, >>> unsigned right shift. Used for efficient flag manipulation and low-level operations.

Code Example

public class BitwiseOperators {
    public static void main(String[] args) {
        int a = 12;  // 1100 in binary
        int b = 10;  // 1010 in binary
        
        System.out.println("a = " + a + " (binary: 1100)");
        System.out.println("b = " + b + " (binary: 1010)");
        
        // AND (1 only if both are 1)
        System.out.println("a & b = " + (a & b));  // 8 (1000)
        
        // OR (1 if either is 1)
        System.out.println("a | b = " + (a | b));  // 14 (1110)
        
        // XOR (1 if different)
        System.out.println("a ^ b = " + (a ^ b));  // 6 (0110)
        
        // NOT (invert bits)
        System.out.println("~a = " + (~a));        // -13
        
        // Left shift (multiply by 2)
        System.out.println("a << 1 = " + (a << 1)); // 24
        
        // Right shift (divide by 2)
        System.out.println("a >> 1 = " + (a >> 1)); // 6
        
        // Unsigned right shift
        int negative = -12;
        System.out.println("-12 >>> 1 = " + (negative >>> 1));
        
        // Bit flags example
        int FLAG_ACTIVE = 1 << 0;    // 0001
        int FLAG_MIGRATING = 1 << 1; // 0010
        int FLAG_NESTING = 1 << 2;   // 0100
        
        int flags = FLAG_ACTIVE | FLAG_MIGRATING;
        System.out.println("\nFlags: " + flags);
        System.out.println("Has active? " + ((flags & FLAG_ACTIVE) != 0));
        System.out.println("Has migrating? " + ((flags & FLAG_MIGRATING) != 0));
        System.out.println("Has nesting? " + ((flags & FLAG_NESTING) != 0));
    }
}

Bitwise operations work on individual bits. Shifts multiply or divide by powers of 2. >>> treats signed integers as unsigned.

3.5 Type Casting & Type Conversion

Type casting converts a value from one data type to another.

Implicit casting (widening) occurs automatically when converting from smaller to larger types. Explicit casting (narrowing) requires manual conversion and may lose data. Type promotion occurs in expressions to avoid data loss.

Code Example

public class TypeCasting {
    public static void main(String[] args) {
        // Implicit casting (widening) - automatic
        int birds = 10;
        double weight = birds;  // int to double (widening)
        System.out.println("int to double: " + weight);
        
        // Explicit casting (narrowing) - manual
        double eagleWeight = 4500.5;
        int eagleWeightInt = (int) eagleWeight;  // double to int (narrowing)
        System.out.println("double to int: " + eagleWeightInt);
        
        // Casting in expressions
        int eagles = 10;
        int hawks = 3;
        double average = (double) eagles / hawks;  // Cast to avoid integer division
        System.out.println("Average: " + average);
        
        // Type promotion
        byte b = 10;
        int i = b + 5;  // b is promoted to int
        System.out.println("byte + int = int: " + i);
        
        // Potential overflow with narrowing
        int large = 1000000;
        short small = (short) large;
        System.out.println("large to short: " + small);  // Overflow
        
        // String to number conversion
        String numberStr = "42";
        int number = Integer.parseInt(numberStr);
        System.out.println("String to int: " + number);
    }
}

Widening is safe and automatic. Narrowing requires explicit casting and may lose data. Type promotion ensures expression evaluation doesn’t lose precision.

3.6 Comments

Comments are text ignored by the compiler, used for documentation and explanation.

Single-line comments use //. Multi-line comments use /* */. Javadoc comments use /** */ and generate API documentation. Good comments explain why, not what.

Code Example

/**
 * This class demonstrates different types of comments in Java.
 * This is a Javadoc comment - used to generate API documentation.
 * 
 * @author BirdWatcher
 * @version 1.0
 */
public class CommentsDemo {
    
    /**
     * This method counts birds.
     * 
     * @param eagles number of eagles
     * @param hawks number of hawks
     * @return total number of birds
     */
    public int countBirds(int eagles, int hawks) {
        // Single-line comment: returns sum of eagles and hawks
        return eagles + hawks;
    }
    
    public static void main(String[] args) {
        // This is a single-line comment
        
        /* This is a multi-line comment
           that can span multiple lines
           for longer explanations */
        
        int birds = 10;  // Comment at the end of a line
        
        /*
         * Multi-line comment with
         * nice formatting using
         * asterisks for readability
         */
        System.out.println("Birds: " + birds);
        
        // TODO: Add exception handling for invalid input
        // FIXME: This calculation might overflow for large numbers
    }
}

Comments are ignored by the compiler. Javadoc comments generate API documentation. Use comments to explain complex logic, not obvious code.

Chapter 4: Control Structures

4.1 Conditional Statements

if, if-else, if-else-if

Conditional statements enable a program to evaluate conditions and choose which block of code to execute based on the resulting outcome.

An if statement runs its associated block of code only when the specified condition evaluates to true. if-else provides an alternative block. if-else-if handles multiple conditions. Curly braces group multiple statements. The dangling else problem is avoided by proper indentation.

Code Example

public class IfStatements {
    public static void main(String[] args) {
        int birdCount = 10;
        int eagleCount = 3;
        int hawkCount = 0;
        
        // Simple if
        if (birdCount > 5) {
            System.out.println("Many birds: " + birdCount);
        }
        
        // if-else
        if (eagleCount > 5) {
            System.out.println("Many eagles: " + eagleCount);
        } else {
            System.out.println("Few eagles: " + eagleCount);
        }
        
        // if-else-if ladder
        if (birdCount > 20) {
            System.out.println("Very large flock");
        } else if (birdCount > 10) {
            System.out.println("Medium flock");
        } else if (birdCount > 5) {
            System.out.println("Small flock");
        } else {
            System.out.println("Tiny flock");
        }
        
        // Nested if-else
        if (eagleCount > 0) {
            if (hawkCount > 0) {
                System.out.println("Both eagles and hawks present");
            } else {
                System.out.println("Only eagles");
            }
        }
        
        // Common mistake: using = instead of ==
        // if (birdCount = 10) {  // ERROR: assignment in condition
        
        // Using && and || in conditions
        if (eagleCount > 0 && hawkCount == 0) {
            System.out.println("Eagles but no hawks");
        }
    }
}

Conditions are evaluated. If true, the associated block executes. In if-else-if, conditions are checked in order until one is true.

switch

The switch statement compares a single expression against multiple constants and executes the matching case.

switch works with primitive types, strings (Java 7+), and enums. case matches the expression value. break exits the switch. default handles unmatched values. Forgetting break causes fall-through.

Code Example

public class SwitchExample {
    public static void main(String[] args) {
        int birdType = 2;
        
        // Traditional switch
        System.out.println("Bird type: " + birdType);
        switch (birdType) {
            case 1:
                System.out.println("Eagle");
                break;
            case 2:
                System.out.println("Sparrow");
                break;
            case 3:
                System.out.println("Hawk");
                break;
            default:
                System.out.println("Unknown bird");
                break;
        }
        
        // Switch with string (Java 7+)
        String bird = "Eagle";
        switch (bird) {
            case "Eagle":
                System.out.println("Large bird of prey");
                break;
            case "Sparrow":
                System.out.println("Small songbird");
                break;
            case "Hawk":
                System.out.println("Medium bird of prey");
                break;
            default:
                System.out.println("Unknown");
        }
        
        // Switch without break (fall-through)
        int count = 5;
        switch (count) {
            case 1:
            case 2:
            case 3:
                System.out.println("Small count");
                break;
            case 4:
            case 5:
            case 6:
                System.out.println("Medium count");
                break;
            default:
                System.out.println("Large count");
        }
        
        // Switch expression (Java 14+)
        String result = switch (birdType) {
            case 1 -> "Eagle";
            case 2 -> "Sparrow";
            case 3 -> "Hawk";
            default -> "Unknown";
        };
        System.out.println("Switch expression: " + result);
    }
}

The expression is evaluated once. Control jumps to the matching case. break exits the switch. Without break, execution continues to the next case.

4.2 Loops

for Loop

The for loop repeats a block a specific number of times, ideal for counting and iteration.

A for loop consists of three main components: initialization, which sets the starting value; condition, which determines whether another iteration should run; and increment, which updates the loop variable after each iteration. The initialization runs once. The condition is checked before each iteration. The increment runs after each iteration. Nested for loops handle multi-dimensional data.

Code Example

public class ForLoop {
    public static void main(String[] args) {
        // Basic for loop (counting up)
        System.out.println("Counting up:");
        for (int i = 1; i <= 5; i++) {
            System.out.println("Bird " + i);
        }
        
        // Counting down
        System.out.println("\nCounting down:");
        for (int i = 5; i >= 1; i--) {
            System.out.println("Bird " + i);
        }
        
        // Step by 2
        System.out.println("\nStep by 2:");
        for (int i = 2; i <= 10; i += 2) {
            System.out.print(i + " ");
        }
        System.out.println();
        
        // Nested for loops
        System.out.println("\nNested loops:");
        for (int row = 1; row <= 3; row++) {
            for (int col = 1; col <= 4; col++) {
                System.out.print(row + "" + col + " ");
            }
            System.out.println();
        }
        
        // Multiple variables
        System.out.println("\nMultiple variables:");
        for (int i = 0, j = 10; i < j; i++, j--) {
            System.out.println("i=" + i + ", j=" + j);
        }
        
        // Common mistake: off-by-one
        System.out.println("\nOff-by-one example:");
        for (int i = 0; i < 5; i++) {  // Correct: 0,1,2,3,4
            System.out.print(i + " ");
        }
        // for (int i = 0; i <= 5; i++) {  // Wrong: includes 5
    }
}

The loop starts with initialization. The condition is checked before each iteration. The body executes if true. The increment runs after each iteration.

while Loop

The while loop repeats a block as long as a condition remains true.

while checks the condition before each iteration. If false initially, the body never executes. Infinite loops occur when the condition never becomes false. Used for input validation and indefinite iteration.

Code Example

public class WhileLoop {
    public static void main(String[] args) {
        // Basic while
        int count = 1;
        System.out.println("Basic while:");
        while (count <= 5) {
            System.out.println("Bird " + count);
            count++;
        }
        
        // Summing numbers
        int sum = 0;
        int num = 1;
        while (num <= 10) {
            sum += num;
            num++;
        }
        System.out.println("Sum 1-10: " + sum);
        
        // Input validation loop
        java.util.Scanner scanner = new java.util.Scanner(System.in);
        int birdCount;
        while (true) {
            System.out.print("Enter bird count (1-10): ");
            birdCount = scanner.nextInt();
            if (birdCount >= 1 && birdCount <= 10) {
                break;  // Exit loop on valid input
            }
            System.out.println("Invalid input! Try again.");
        }
        System.out.println("Valid count: " + birdCount);
        
        // Common mistake: infinite loop
        // int i = 0;
        // while (i < 5) {
        //     System.out.println(i);
        //     // i++;  // Missing increment causes infinite loop
        // }
        
        scanner.close();
    }
}

The condition is evaluated before each iteration. If true, the body executes. If false, the loop ends. Variables must be updated inside the loop.

do-while Loop

A do-while loop runs its body once before evaluating the condition, ensuring that the code executes at least one time.

do-while executes the body first, then checks the condition. Guarantees at least one execution. Syntax requires a semicolon after the condition. Used when the body must run before checking.

Code Example

public class DoWhileLoop {
    public static void main(String[] args) {
        // Basic do-while
        int count = 1;
        System.out.println("do-while:");
        do {
            System.out.println("Bird " + count);
            count++;
        } while (count <= 5);
        
        // Menu loop (always runs at least once)
        java.util.Scanner scanner = new java.util.Scanner(System.in);
        int choice;
        System.out.println("\nMenu:");
        System.out.println("1. Add bird");
        System.out.println("2. Remove bird");
        System.out.println("3. Exit");
        
        do {
            System.out.print("Enter choice: ");
            choice = scanner.nextInt();
            switch (choice) {
                case 1:
                    System.out.println("Adding bird...");
                    break;
                case 2:
                    System.out.println("Removing bird...");
                    break;
                case 3:
                    System.out.println("Exiting...");
                    break;
                default:
                    System.out.println("Invalid choice");
            }
        } while (choice != 3);
        
        // Input validation with do-while
        int birdCount;
        do {
            System.out.print("Enter bird count (1-10): ");
            birdCount = scanner.nextInt();
        } while (birdCount < 1 || birdCount > 10);
        System.out.println("Valid count: " + birdCount);
        
        scanner.close();
    }
}

The body executes once, then the condition is checked. If true, the body repeats. If false, the loop ends. The semicolon after the condition is required.

Enhanced for Loop (for-each)

The enhanced for loop provides a simpler way to iterate over arrays and collections.

The for-each loop works with arrays and collections implementing Iterable. Syntax: for (Type variable : collection). Cannot modify the collection during iteration. No index access.

Code Example

import java.util.ArrayList;

public class EnhancedForLoop {
    public static void main(String[] args) {
        // Array iteration
        String[] birds = {"Sparrow", "Eagle", "Hawk", "Cardinal"};
        
        System.out.println("Array with enhanced for:");
        for (String bird : birds) {
            System.out.println("- " + bird);
        }
        
        // ArrayList iteration
        ArrayList<Integer> counts = new ArrayList<>();
        counts.add(10);
        counts.add(20);
        counts.add(30);
        
        System.out.println("\nArrayList with enhanced for:");
        for (int count : counts) {
            System.out.println("Count: " + count);
        }
        
        // Using with arrays of primitive types
        int[] numbers = {1, 2, 3, 4, 5};
        System.out.println("\nSumming numbers:");
        int sum = 0;
        for (int num : numbers) {
            sum += num;
        }
        System.out.println("Sum: " + sum);
        
        // Cannot modify collection during iteration
        // for (String bird : birds) {
        //     bird = "Modified";  // Doesn't modify the array
        // }
        
        // Cannot get index in for-each loop
        // Use regular for loop when index needed
        for (int i = 0; i < birds.length; i++) {
            System.out.println("Bird " + i + ": " + birds[i]);
        }
    }
}

The enhanced for loop creates an iterator internally. It accesses the elements sequentially, processing each one individually.. The variable holds each element’s value. The loop works with any Iterable or array.

4.3 Loop Control Statements

break

The break statement immediately exits the innermost loop or switch block.

break terminates the loop immediately. In nested loops, it exits only the innermost loop.A labeled break allows a program to exit a designated outer loop directly, providing a way to terminate nested loops early.

Code Example

public class BreakExample {
    public static void main(String[] args) {
        // Finding a value
        int[] birds = {10, 20, 30, 40, 50};
        int target = 30;
        boolean found = false;
        
        System.out.println("Searching for " + target + ":");
        for (int i = 0; i < birds.length; i++) {
            if (birds[i] == target) {
                System.out.println("Found at index " + i);
                found = true;
                break;  // Exit loop when found
            }
            System.out.println("Checking " + birds[i] + "...");
        }
        if (!found) {
            System.out.println("Not found");
        }
        
        // Break in nested loops
        System.out.println("\nNested break:");
        outerLoop:
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 5; j++) {
                if (i == 2 && j == 3) {
                    break outerLoop;  // Breaks outer loop
                }
                System.out.print(i + "," + j + " ");
            }
            System.out.println();
        }
        
        // Break in switch
        int choice = 2;
        switch (choice) {
            case 1:
                System.out.println("Eagle");
                break;
            case 2:
                System.out.println("Sparrow");
                break;  // Prevents fall-through
            case 3:
                System.out.println("Hawk");
                break;
            default:
                System.out.println("Unknown");
        }
    }
}

break exits the current loop or switch. In nested loops, only the innermost loop is affected. Labeled breaks can exit specific outer loops.

continue

The continue statement skips the rest of the current iteration and moves to the next.

continue jumps to the next iteration. In for loops, it executes the increment step. In while loops, it jumps to the condition check. Used to skip certain values.

Code Example

public class ContinueExample {
    public static void main(String[] args) {
        // Skip even numbers
        System.out.println("Odd numbers:");
        for (int i = 1; i <= 10; i++) {
            if (i % 2 == 0) {
                continue;  // Skip even numbers
            }
            System.out.print(i + " ");
        }
        System.out.println();
        
        // Skip specific values
        System.out.println("\nSkip 5, 8:");
        for (int i = 1; i <= 10; i++) {
            if (i == 5 || i == 8) {
                continue;
            }
            System.out.print(i + " ");
        }
        System.out.println();
        
        // Continue in while
        System.out.println("\nWhile with continue:");
        int count = 0;
        while (count < 10) {
            count++;
            if (count % 3 == 0) {
                continue;  // Skip multiples of 3
            }
            System.out.print(count + " ");
        }
        System.out.println();
        
        // Continue in nested loops
        System.out.println("\nNested continue:");
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (j == 2) {
                    continue;  // Skips j=2 for each i
                }
                System.out.print(i + "," + j + " ");
            }
            System.out.println();
        }
    }
}

continue skips the remaining code in the current iteration. In for loops, the increment still executes. In while loops, it jumps to the condition check.

return

The return statement exits a method and optionally returns a value.

return immediately exits the method.A return statement can provide a value that matches the method’s declared return type. In a void method, return; can be used without a value to end the method before reaching its final statement.Without return, the method ends when the last statement executes.

Code Example

public class ReturnExample {
    // Method returning int
    public static int countBirds(int eagles, int hawks) {
        return eagles + hawks;  // Returns sum
    }
    
    // Method returning String
    public static String getBirdType(int code) {
        if (code == 1) {
            return "Eagle";
        } else if (code == 2) {
            return "Sparrow";
        } else {
            return "Unknown";
        }
    }
    
    // Method with early return
    public static int validateCount(int count) {
        if (count < 0) {
            return -1;  // Early return for invalid input
        }
        if (count > 100) {
            return 100;  // Early return for cap
        }
        return count;  // Return original if valid
    }
    
    // void method with return
    public static void displayIfPositive(int count) {
        if (count <= 0) {
            return;  // Early exit from void method
        }
        System.out.println("Count: " + count);
    }
    
    public static void main(String[] args) {
        int total = countBirds(5, 3);
        System.out.println("Total birds: " + total);
        
        String type = getBirdType(2);
        System.out.println("Bird type: " + type);
        
        int validated = validateCount(150);
        System.out.println("Validated: " + validated);
        
        displayIfPositive(10);    // Prints
        displayIfPositive(-5);    // Nothing printed
    }
}

return exits the method immediately. For a non-void method, the execution path must provide a value compatible with the method’s declared return type. A void method does not return a value, but return; can be used to stop its execution before reaching the end.

Chapter 5: Functions / Methods

5.1 Defining Methods

Methods are defined sections of code designed to carry out particular tasks or operations within a program. They promote code reuse and organization.

A method has a signature: modifiers, return type, name, parameters, and body. Method call invokes the method. Method chaining allows multiple method calls to be linked together so they execute sequentially as part of a single expression. Methods can be static (belong to class) or instance (belong to objects).

Code Example

public class MethodExample {
    // Method with parameters and return value
    public static int addBirds(int sparrow, int eagle) {
        return sparrow + eagle;
    }
    
    // Method without parameters
    public static void displayWelcome() {
        System.out.println("Welcome to Bird Counter!");
    }
    
    // Method with void return type
    public static void displayCount(String species, int count) {
        System.out.println(species + ": " + count);
    }
    
    // Method returning double
    public static double averageWeight(int count, double totalWeight) {
        if (count == 0) return 0;
        return totalWeight / count;
    }
    
    // Method chaining example
    public static void chainExample() {
        displayWelcome();
        int total = addBirds(5, 3);
        displayCount("Total", total);
    }
    
    public static void main(String[] args) {
        // Calling methods
        displayWelcome();
        
        int sparrows = 10;
        int eagles = 3;
        int total = addBirds(sparrows, eagles);
        displayCount("Total birds", total);
        
        double avg = averageWeight(total, 25.5);
        System.out.println("Average weight: " + avg);
        
        // Method chaining
        chainExample();
    }
}

Methods are defined with a signature. Methods are invoked using their names, with any required arguments supplied in the call. They then execute their defined instructions and may either return a value or produce no result when declared void. Static methods are associated with the class itself rather than with individual objects.

5.2 Method Components

Access Modifiers (public, private, protected, default)

Access modifiers control visibility of classes, methods, and fields.

public accessible everywhere. The private access modifier restricts a member so that it can be accessed only from within the class where it is declared. protected accessible in same package and subclasses. Default (package-private) accessible in the same package.

Code Example

package bird;

public class Bird {
    // Public - accessible everywhere
    public String species;
    
    // Private - only accessible in this class
    private int count;
    
    // Protected - accessible in package and subclasses
    protected double weight;
    
    // Default (no modifier) - accessible in same package
    String habitat;
    
    // Public method
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);  // Accessible here
    }
    
    // Private method - only accessible in this class
    private void validateCount(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("Count cannot be negative");
        }
    }
    
    // Protected method
    protected void setWeight(double weight) {
        this.weight = weight;
    }
}

// In another package
class BirdTest {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.species = "Sparrow";  // Works - public
        // bird.count = 10;        // ERROR - private
        // bird.weight = 25.5;     // ERROR - protected (different package)
        // bird.habitat = "Forest"; // ERROR - default (different package)
    }
}

Access modifiers control visibility. Among Java’s common access modifiers, private provides the most limited access, while public allows the broadest access. Use private for encapsulation, public for the API.

Return Type and void

Return types specify what value a method returns. void means no return value.

Methods can return any data type. void methods perform actions without returning a value. Non-void methods must return a value matching the declared type. Early returns can exit methods early.

Code Example

public class ReturnTypes {
    // void method - no return value
    public static void displayMessage(String msg) {
        System.out.println(msg);
        // No return statement needed
    }
    
    // Method returning int
    public static int getBirdCount() {
        return 10;  // Must return int
    }
    
    // Method returning String
    public static String getBirdName(int id) {
        if (id == 1) {
            return "Eagle";
        } else if (id == 2) {
            return "Sparrow";
        } else {
            return "Unknown";
        }
    }
    
    // Method returning boolean
    public static boolean isValidCount(int count) {
        return count >= 0 && count <= 100;
    }
    
    // Method returning double
    public static double getAverageWeight(double totalWeight, int count) {
        if (count == 0) {
            return 0.0;  // Early return
        }
        return totalWeight / count;
    }
    
    public static void main(String[] args) {
        displayMessage("Hello, World!");
        
        int count = getBirdCount();
        System.out.println("Bird count: " + count);
        
        String name = getBirdName(1);
        System.out.println("Bird name: " + name);
        
        boolean valid = isValidCount(50);
        System.out.println("Valid: " + valid);
        
        double avg = getAverageWeight(25.5, 5);
        System.out.println("Average weight: " + avg);
    }
}

void methods don’t return a value. A non-void method must provide a result that matches its declared return type. The return statement ends the method’s execution and sends that result back to the calling code.

Parameters

Parameters are variables that receive values passed to a method.

Parameters are declared in the method signature. Arguments are the values passed when calling. Varargs (...) allow variable number of arguments. Parameters are variables defined in a method’s parameter list that act as local variables while the method is executing.

Code Example

public class Parameters {
    // Method with no parameters
    public static void displayWelcome() {
        System.out.println("Welcome!");
    }
    
    // Method with one parameter
    public static void displayBird(String species) {
        System.out.println("Bird: " + species);
    }
    
    // Method with multiple parameters
    public static int addBirds(int sparrows, int eagles, int hawks) {
        return sparrows + eagles + hawks;
    }
    
    // Method with varargs (variable arguments)
    public static int countBirds(String species, int... counts) {
        int total = 0;
        for (int count : counts) {
            total += count;
        }
        System.out.println("Species: " + species);
        System.out.println("Total: " + total);
        return total;
    }
    
    // Method with different parameter types
    public static void displayBirdInfo(String species, int count, double weight, boolean isMigratory) {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
        System.out.println("Migratory: " + isMigratory);
    }
    
    public static void main(String[] args) {
        displayWelcome();
        displayBird("Sparrow");
        
        int total = addBirds(10, 3, 5);
        System.out.println("Total: " + total);
        
        // Varargs - can pass any number of arguments
        countBirds("Eagle", 3, 2, 1);
        countBirds("Sparrow", 10, 20, 30, 40);
        
        displayBirdInfo("Hawk", 5, 1200.0, true);
    }
}

Parameters are declared in the method signature. Arguments are passed when calling. Varargs (...) allow flexible argument lists. Parameters are local to the method.

5.3 Passing Parameters

Pass-by-Value

Java is pass-by-value. Parameters receive copies of the argument values.

For primitives, the copy holds the actual value. For objects, the copy holds the reference (address). Modifying the object through a reference affects the original, but reassigning the reference doesn’t.

Code Example

public class PassByValue {
    // Method that modifies a primitive (doesn't affect original)
    public static void modifyPrimitive(int value) {
        value = value * 2;
        System.out.println("Inside method: " + value);
    }
    
    // Method that modifies object fields (affects original)
    public static void modifyObject(Bird bird) {
        bird.count = 20;
        System.out.println("Inside method: " + bird.count);
    }
    
    // Method that reassigns object reference (doesn't affect original)
    public static void reassignObject(Bird bird) {
        bird = new Bird("New", 50);
        System.out.println("Inside method: " + bird.count);
    }
    
    public static void main(String[] args) {
        // Primitive example
        int count = 10;
        System.out.println("Before: " + count);
        modifyPrimitive(count);
        System.out.println("After (unchanged): " + count);
        
        // Object field modification
        Bird sparrow = new Bird("Sparrow", 10);
        System.out.println("\nBefore: " + sparrow.count);
        modifyObject(sparrow);
        System.out.println("After (changed): " + sparrow.count);
        
        // Object reference reassignment
        System.out.println("\nBefore: " + sparrow.count);
        reassignObject(sparrow);
        System.out.println("After (unchanged): " + sparrow.count);
    }
}

class Bird {
    String species;
    int count;
    
    Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
}

Primitive values are copied. Object references are copied but point to the same object. Modifying object fields affects the original. Reassigning the reference doesn’t affect the original.

Varargs

Varargs allow a method to receive a flexible number of arguments of the same type through a single parameter.

Varargs use ... syntax. They are treated as arrays inside the method. Only one vararg parameter is allowed per method, and it must be the last parameter.

Code Example

public class VarargsExample {
    // Basic varargs
    public static int sum(int... numbers) {
        int total = 0;
        for (int num : numbers) {
            total += num;
        }
        return total;
    }
    
    // Varargs with other parameters
    public static void displayBirds(String type, int... counts) {
        System.out.println("Bird type: " + type);
        System.out.print("Counts: ");
        for (int count : counts) {
            System.out.print(count + " ");
        }
        System.out.println();
    }
    
    // Varargs with mixed types
    public static void printInfo(String name, double weight, int... counts) {
        System.out.println("Name: " + name);
        System.out.println("Weight: " + weight);
        System.out.print("Counts: ");
        for (int count : counts) {
            System.out.print(count + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        // Call with different numbers of arguments
        System.out.println("Sum 1,2,3: " + sum(1, 2, 3));
        System.out.println("Sum 1,2,3,4,5: " + sum(1, 2, 3, 4, 5));
        System.out.println("Sum empty: " + sum());  // 0
        
        displayBirds("Sparrow", 10, 20, 30);
        displayBirds("Eagle", 3, 2, 1);
        
        printInfo("Hawk", 1200.0, 5, 3, 2);
    }
}

Varargs are compiled into arrays. They can accept zero or more arguments. They must be the last parameter. Used for flexible method arguments.

5.4 Method Overloading

Method overloading allows multiple methods with the same name but different parameters.

Methods are overloaded by different number of parameters, different parameter types, or different order. Return type alone cannot overload methods. The compiler selects the matching method.

Code Example

public class OverloadingExample {
    // Overloaded methods by number of parameters
    public static void display(String species) {
        System.out.println("Species: " + species);
    }
    
    public static void display(String species, int count) {
        System.out.println("Species: " + species + ", Count: " + count);
    }
    
    public static void display(String species, int count, double weight) {
        System.out.println("Species: " + species + ", Count: " + count + ", Weight: " + weight);
    }
    
    // Overloaded by parameter type
    public static void process(int count) {
        System.out.println("Processing int: " + count);
    }
    
    public static void process(double weight) {
        System.out.println("Processing double: " + weight);
    }
    
    public static void process(String species) {
        System.out.println("Processing String: " + species);
    }
    
    // Overloaded by parameter order
    public static void info(int count, String species) {
        System.out.println(count + " " + species);
    }
    
    public static void info(String species, int count) {
        System.out.println(species + " " + count);
    }
    
    // Cannot overload by return type only
    // public static int load() { return 0; }
    // public static String load() { return ""; }  // ERROR
    
    public static void main(String[] args) {
        // Calls different overloaded methods
        display("Sparrow");
        display("Eagle", 3);
        display("Hawk", 5, 1200.0);
        
        process(10);
        process(25.5);
        process("Sparrow");
        
        info(3, "Eagles");
        info("Hawks", 5);
    }
}

The compiler selects the best match based on arguments. Overloading provides flexibility and intuitive method names. It’s resolved at compile time.

5.5 Recursion

Recursion is a technique where a method calls itself to solve a problem.

Recursive methods have a base case (stopping condition) and a recursive case. Without a base case, recursion continues indefinitely. Stack overflow occurs if recursion is too deep.

Code Example

public class RecursionExample {
    // Recursive factorial
    public static int factorial(int n) {
        if (n <= 1) {  // Base case
            return 1;
        }
        return n * factorial(n - 1);  // Recursive case
    }
    
    // Recursive Fibonacci
    public static int fibonacci(int n) {
        if (n <= 1) {  // Base cases
            return n;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    
    // Recursive array sum
    public static int sumArray(int[] arr, int index) {
        if (index >= arr.length) {  // Base case
            return 0;
        }
        return arr[index] + sumArray(arr, index + 1);
    }
    
    // Recursive binary search
    public static int binarySearch(int[] arr, int target, int left, int right) {
        if (left > right) {  // Base case - not found
            return -1;
        }
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            return binarySearch(arr, target, mid + 1, right);
        } else {
            return binarySearch(arr, target, left, mid - 1);
        }
    }
    
    public static void main(String[] args) {
        // Factorial
        System.out.println("5! = " + factorial(5));
        System.out.println("10! = " + factorial(10));
        
        // Fibonacci
        System.out.println("Fib(7) = " + fibonacci(7));
        for (int i = 0; i < 8; i++) {
            System.out.print("Fib(" + i + ")=" + fibonacci(i) + " ");
        }
        System.out.println();
        
        // Array sum
        int[] birds = {10, 20, 30, 40, 50};
        System.out.println("Sum: " + sumArray(birds, 0));
        
        // Binary search
        int[] sorted = {5, 12, 23, 32, 45, 62, 78, 91};
        int index = binarySearch(sorted, 45, 0, sorted.length - 1);
        System.out.println("Found 45 at index: " + index);
    }
}

The method calls itself with modified arguments. Each call uses stack memory. The base case stops the recursion. Without a base case, stack overflow occurs.

5.6 Static Methods

Static methods are associated with the class itself rather than individual objects. They can be invoked directly through the class name without first creating an instance.

Static methods are invoked through the class and can directly access only static members. Instance methods, on the other hand, are called through an object and can work with both instance and static members.

The main() method is declared static so the JVM can start program execution without first creating an object of the class.

Code Example

public class StaticMethods {
    // Static variable
    static int totalBirds = 0;
    
    // Instance variable
    int localCount;
    
    // Static method
    public static int getTotalBirds() {
        return totalBirds;  // Can access static members
    }
    
    // Static method with parameters
    public static void addBirds(int count) {
        totalBirds += count;
        // localCount = count;  // ERROR: Cannot access instance member
    }
    
    // Instance method
    public void setLocalCount(int count) {
        this.localCount = count;
        totalBirds += count;  // Can access static members
    }
    
    // Static method with return
    public static String getStatus() {
        return "Total birds: " + totalBirds;
    }
    
    public static void main(String[] args) {
        // Calling static methods
        StaticMethods.addBirds(10);
        StaticMethods.addBirds(5);
        System.out.println("Total: " + StaticMethods.getTotalBirds());
        System.out.println(StaticMethods.getStatus());
        
        // Can also call static methods without class name in the same class
        addBirds(3);
        System.out.println("Total after adding: " + getTotalBirds());
        
        // Creating instance for instance methods
        StaticMethods obj = new StaticMethods();
        obj.setLocalCount(20);
        System.out.println("Local count: " + obj.localCount);
        System.out.println("Total: " + totalBirds);
    }
}

Static methods belong to the class. They can access only static members. main() is static. Instance methods require an object and can access all members.

Chapter 6: Object-Oriented Programming (OOP)

6.1 Fundamentals

Encapsulation, Inheritance, Polymorphism, Abstraction

The four pillars of OOP provide the foundation for object-oriented design.

Encapsulation hides internal details and protects data. Inheritance allows code reuse and establishes relationships. Polymorphism enables flexible, extensible code. Abstraction simplifies interfaces and hides complexity.

Code Example

// ---- ENCAPSULATION ----
class Bird {
    private String species;  // Private data
    private int count;
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    // Getters and setters (controlled access)
    public String getSpecies() { return species; }
    public int getCount() { return count; }
    public void setCount(int count) {
        if (count >= 0) this.count = count;  // Validation
    }
}

// ---- INHERITANCE ----
class Eagle extends Bird {
    private double wingSpan;
    
    public Eagle(String species, int count, double wingSpan) {
        super(species, count);  // Call parent constructor
        this.wingSpan = wingSpan;
    }
    
    public double getWingSpan() { return wingSpan; }
}

// ---- POLYMORPHISM ----
class Sparrow extends Bird {
    public Sparrow(String species, int count) {
        super(species, count);
    }
    
    public String getSound() {
        return "Chirp!";
    }
}

public class OOPConcepts {
    public static void main(String[] args) {
        // Encapsulation in action
        Bird bird = new Bird("Sparrow", 10);
        bird.setCount(15);  // Controlled access
        System.out.println("Species: " + bird.getSpecies());
        System.out.println("Count: " + bird.getCount());
        
        // Inheritance
        Eagle eagle = new Eagle("Golden Eagle", 3, 2.3);
        System.out.println("\nEagle: " + eagle.getSpecies());
        System.out.println("Count: " + eagle.getCount());
        System.out.println("Wingspan: " + eagle.getWingSpan());
        
        // Polymorphism
        Sparrow sparrow = new Sparrow("House Sparrow", 20);
        System.out.println("\nSparrow sound: " + sparrow.getSound());
        
        // Abstraction - using interface
        Flyable flyingBird = new Eagle("Bald Eagle", 2, 2.0);
        flyingBird.fly();  // Abstract interface
    }
}

// ---- ABSTRACTION ----
interface Flyable {
    void fly();  // Abstract method
}

class Eagle extends Bird implements Flyable {
    // ... previous Eagle code
    @Override
    public void fly() {
        System.out.println("Eagle soaring high!");
    }
}

Encapsulation protects data with private fields and public methods. Inheritance extends parent classes. Polymorphism allows treating objects by their supertype. Abstraction simplifies interfaces.

6.2 Classes & Objects

A class is a blueprint for objects.An object is a specific instance created from a class, representing a concrete entity with its own state and behavior.

A class defines fields (data) and methods (behavior). An object is created using the new keyword. Fields store the object’s state. Methods define its behavior.

Code Example

// Class definition
class Bird {
    // Fields (state)
    private String species;
    private int count;
    private double weight;
    
    // Constructor
    public Bird(String species, int count, double weight) {
        this.species = species;
        this.count = count;
        this.weight = weight;
    }
    
    // Methods (behavior)
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight + " grams");
    }
    
    public void addBirds(int number) {
        if (number > 0) {
            count += number;
            System.out.println("Added " + number + " birds");
        }
    }
    
    // Getters
    public String getSpecies() { return species; }
    public int getCount() { return count; }
    public double getWeight() { return weight; }
}

public class ClassesAndObjects {
    public static void main(String[] args) {
        // Creating objects
        Bird sparrow = new Bird("Sparrow", 10, 25.5);
        Bird eagle = new Bird("Eagle", 3, 4500.0);
        
        // Using objects
        System.out.println("=== Sparrow ===");
        sparrow.display();
        sparrow.addBirds(5);
        System.out.println("Updated count: " + sparrow.getCount());
        
        System.out.println("\n=== Eagle ===");
        eagle.display();
        
        // Array of objects
        Bird[] birds = new Bird[3];
        birds[0] = sparrow;
        birds[1] = eagle;
        birds[2] = new Bird("Hawk", 5, 1200.0);
        
        System.out.println("\n=== Bird List ===");
        for (Bird b : birds) {
            System.out.println(b.getSpecies() + ": " + b.getCount());
        }
    }
}

Classes define the structure and behavior. Objects are created with new. Each object maintains its own set of values for its instance fields, allowing different objects of the same class to hold different states. Methods operate on the object’s data.

6.3 Constructors

Default Constructor

The default constructor is automatically provided if no constructor is defined. It initializes fields to default values.

The default constructor has no parameters. It’s provided by the compiler only if no constructor is defined.If a class declares its own constructor, the compiler does not automatically provide a no-argument default constructor.

Code Example

class Bird {
    String species;
    int count;
    double weight;
    
    // No constructor defined - default provided
}

class BirdWithConstructor {
    String species;
    int count;
    double weight;
    
    // Parameterized constructor - no default
    BirdWithConstructor(String species, int count, double weight) {
        this.species = species;
        this.count = count;
        this.weight = weight;
    }
}

public class DefaultConstructor {
    public static void main(String[] args) {
        // Uses default constructor
        Bird bird1 = new Bird();
        System.out.println("Default values:");
        System.out.println("Species: " + bird1.species);  // null
        System.out.println("Count: " + bird1.count);      // 0
        System.out.println("Weight: " + bird1.weight);    // 0.0
        
        // Must use parameterized constructor
        BirdWithConstructor bird2 = new BirdWithConstructor("Eagle", 3, 4500.0);
        System.out.println("\nWith constructor:");
        System.out.println("Species: " + bird2.species);
        System.out.println("Count: " + bird2.count);
        System.out.println("Weight: " + bird2.weight);
        
        // Error: BirdWithConstructor bird3 = new BirdWithConstructor();
        // No default constructor provided
    }
}

The default constructor is provided automatically if no constructor is defined. It initializes fields to default values (0, null, false). Defining any constructor removes the default.

Parameterized Constructor

Parameterized constructors accept arguments to initialize objects with specific values.

Parameterized constructors allow custom initialization. They are used to set initial field values. Multiple constructors can be overloaded with different parameters.

Code Example

class Bird {
    private String species;
    private int count;
    private double weight;
    private boolean isMigratory;
    
    // Constructor with 2 parameters
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
        this.weight = 0.0;
        this.isMigratory = false;
        System.out.println("Bird created: " + species);
    }
    
    // Constructor with 3 parameters
    public Bird(String species, int count, double weight) {
        this.species = species;
        this.count = count;
        this.weight = weight;
        this.isMigratory = false;
        System.out.println("Bird with weight created: " + species);
    }
    
    // Constructor with all parameters
    public Bird(String species, int count, double weight, boolean isMigratory) {
        this.species = species;
        this.count = count;
        this.weight = weight;
        this.isMigratory = isMigratory;
        System.out.println("Full bird created: " + species);
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
        System.out.println("Migratory: " + isMigratory);
    }
}

public class ParameterizedConstructor {
    public static void main(String[] args) {
        // Using different constructors
        Bird sparrow = new Bird("Sparrow", 10);
        Bird eagle = new Bird("Eagle", 3, 4500.0);
        Bird hawk = new Bird("Hawk", 5, 1200.0, true);
        
        System.out.println("\nSparrow:");
        sparrow.display();
        System.out.println("\nEagle:");
        eagle.display();
        System.out.println("\nHawk:");
        hawk.display();
    }
}

Parameterized constructors initialize objects with custom values. Overloaded constructors provide flexibility. this distinguishes parameters from fields.

Copy Constructor

A copy constructor initializes a newly created object using the state of an existing object of the same class.

A copy constructor receives an existing object of the same class as its parameter and uses its data to initialize the new object. It copies field values to the new object. Useful for creating independent copies (deep copy).

Code Example

class Bird {
    private String species;
    private int count;
    private double weight;
    private String[] habitats;  // Array for deep copy example
    
    // Regular constructor
    public Bird(String species, int count, double weight, String[] habitats) {
        this.species = species;
        this.count = count;
        this.weight = weight;
        this.habitats = habitats;
    }
    
    // Copy constructor (shallow copy)
    public Bird(Bird other) {
        this.species = other.species;
        this.count = other.count;
        this.weight = other.weight;
        // Shallow copy of array - both objects share the array
        this.habitats = other.habitats;
    }
    
    // Copy constructor (deep copy) - preferred
    public Bird deepCopy(Bird other) {
        this.species = other.species;
        this.count = other.count;
        this.weight = other.weight;
        // Deep copy of array - new array for new object
        if (other.habitats != null) {
            this.habitats = new String[other.habitats.length];
            for (int i = 0; i < other.habitats.length; i++) {
                this.habitats[i] = other.habitats[i];
            }
        }
        return this;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
        if (habitats != null) {
            System.out.print("Habitats: ");
            for (String h : habitats) {
                System.out.print(h + " ");
            }
            System.out.println();
        }
    }
    
    public void setCount(int count) { this.count = count; }
    public void setHabitats(String[] habitats) { this.habitats = habitats; }
}

public class CopyConstructorExample {
    public static void main(String[] args) {
        String[] habitats = {"Forest", "Fields", "Wetlands"};
        Bird original = new Bird("Sparrow", 10, 25.5, habitats);
        
        // Using copy constructor
        Bird copy = new Bird(original);
        
        System.out.println("Original:");
        original.display();
        System.out.println("\nCopy:");
        copy.display();
        
        // Modifying copy doesn't affect original (for primitives)
        copy.setCount(20);
        System.out.println("\nAfter modifying copy count:");
        System.out.println("Original count: " + original.getCount());
        System.out.println("Copy count: " + copy.getCount());
        
        // But shallow copy shares array reference
        String[] newHabitats = {"Coastal"};
        copy.setHabitats(newHabitats);
        System.out.println("\nAfter modifying copy habitats:");
        System.out.println("Original: " + original.habitats[0]);  // Changes!
        System.out.println("Copy: " + copy.habitats[0]);
    }
}

Copy constructors create new objects from existing ones. Shallow copy copies references. Deep copy creates new copies of referenced objects. Deep copy is safer for mutable objects.

Constructor Chaining (this() and super())

Constructor chaining calls one constructor from another using this() or super().

The this() call invokes another constructor within the same class, while super() invokes a constructor from the parent class. When used, either constructor call must appear as the first statement in the constructor. This approach helps centralize initialization logic and minimize repeated code.

Code Example

class Animal {
    String type;
    
    Animal(String type) {
        this.type = type;
        System.out.println("Animal constructor: " + type);
    }
}

class Bird extends Animal {
    String species;
    int count;
    double weight;
    
    // Constructor 1
    Bird(String species, int count) {
        this(species, count, 0.0);  // Calls constructor 3
        System.out.println("Bird 2-param constructor");
    }
    
    // Constructor 2
    Bird(String species, int count, double weight) {
        super(species);  // Calls parent constructor
        this.species = species;
        this.count = count;
        this.weight = weight;
        System.out.println("Bird 3-param constructor");
    }
    
    // Constructor 3
    Bird(String species) {
        this(species, 0);  // Calls constructor 1
        System.out.println("Bird 1-param constructor");
    }
    
    void display() {
        System.out.println("Type: " + type);
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
    }
}

public class ConstructorChaining {
    public static void main(String[] args) {
        System.out.println("Creating eagle with 2 params:");
        Bird eagle = new Bird("Eagle", 3);
        eagle.display();
        
        System.out.println("\nCreating sparrow with 1 param:");
        Bird sparrow = new Bird("Sparrow");
        sparrow.display();
    }
}

this() chains to another constructor in the same class. super() calls the parent constructor. They must be the first statement. Constructor chaining reduces code duplication.

6.4 Static Members

Static Variables

Static variables belong to the class, not individual objects. All instances share the same static variable.

Static variables are declared with static. They exist even without objects. Used for constants, counters, and shared state. Accessed via class name.

Code Example

class Bird {
    private String species;
    private int count;
    
    // Static variable - shared across all instances
    public static int totalBirds = 0;
    
    // Static constant
    public static final String CATEGORY = "Aves";
    
    // Static counter
    private static int instanceCount = 0;
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
        totalBirds += count;
        instanceCount++;
        System.out.println("Bird created. Total: " + totalBirds);
    }
    
    // Static method to access static variable
    public static int getTotalBirds() {
        return totalBirds;
    }
    
    public static int getInstanceCount() {
        return instanceCount;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Category: " + CATEGORY);
    }
}

public class StaticVariables {
    public static void main(String[] args) {
        // Access static variable before creating objects
        System.out.println("Initial total: " + Bird.totalBirds);
        System.out.println("Category: " + Bird.CATEGORY);
        
        // Create objects
        Bird sparrow = new Bird("Sparrow", 10);
        Bird eagle = new Bird("Eagle", 3);
        Bird hawk = new Bird("Hawk", 5);
        
        // Static variables are shared
        System.out.println("\nTotal birds: " + Bird.getTotalBirds());
        System.out.println("Instances: " + Bird.getInstanceCount());
        
        // Modifying static variable
        Bird.totalBirds = 0;  // Resets for all
        System.out.println("After reset: " + Bird.getTotalBirds());
    }
}

Static variables are shared by all instances. They are initialized once when the class loads. Accessed using the class name. Use for constants and shared state.

Static Methods

Static methods belong to the class and can be called without creating objects.

Static methods can access only static members. They are called using the class name. Common in utility classes. main() is static.

Code Example

class MathUtils {
    // Static method
    public static int add(int a, int b) {
        return a + b;
    }
    
    public static int multiply(int a, int b) {
        return a * b;
    }
    
    public static int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);  // Can call static method
    }
    
    // Cannot access instance members
    // int instanceVar = 0;
    // public static void method() { 
    //     instanceVar = 5;  // ERROR
    // }
}

class Bird {
    private static int total = 0;
    private int localCount;
    
    // Static method
    public static int getTotal() {
        return total;
    }
    
    // Instance method
    public void setLocalCount(int count) {
        this.localCount = count;
        total += count;  // Can access static from instance
    }
}

public class StaticMethods {
    public static void main(String[] args) {
        // Calling static methods without objects
        System.out.println("Add: " + MathUtils.add(10, 5));
        System.out.println("Multiply: " + MathUtils.multiply(10, 5));
        System.out.println("Factorial: " + MathUtils.factorial(5));
        
        // Static method in Bird
        System.out.println("Total: " + Bird.getTotal());
        
        Bird bird = new Bird();
        bird.setLocalCount(10);
        System.out.println("Total after instance method: " + Bird.getTotal());
    }
}

Static methods belong to the class. They can only access static members. Called using the class name. Utility methods are often static.

Static Blocks

Static blocks are executed when the class is loaded, once per class.

Static blocks initialize static members.They are executed when the class is initialized by the Java runtime, typically during its first active use. Multiple static blocks execute in order. Used for complex initialization.

Code Example

import java.util.ArrayList;
import java.util.List;

class Bird {
    private String species;
    private int count;
    
    // Static variables
    private static List<String> knownSpecies;
    private static int totalBirds;
    private static String configFile;
    
    // Static block 1 - runs first
    static {
        System.out.println("Static block 1");
        knownSpecies = new ArrayList<>();
        knownSpecies.add("Sparrow");
        knownSpecies.add("Eagle");
        knownSpecies.add("Hawk");
        totalBirds = 0;
    }
    
    // Static block 2 - runs second
    static {
        System.out.println("Static block 2");
        configFile = "bird_config.properties";
        System.out.println("Loaded species: " + knownSpecies.size());
    }
    
    // Instance block - runs before constructor
    {
        System.out.println("Instance block for " + species);
    }
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
        totalBirds += count;
        System.out.println("Constructor: " + species);
    }
    
    public static void displayConfig() {
        System.out.println("Known species: " + knownSpecies);
        System.out.println("Total birds: " + totalBirds);
        System.out.println("Config file: " + configFile);
    }
}

public class StaticBlocks {
    public static void main(String[] args) {
        System.out.println("Main method started");
        
        // Static blocks run before main
        Bird.displayConfig();
        
        System.out.println("\nCreating birds:");
        Bird sparrow = new Bird("Sparrow", 10);
        Bird eagle = new Bird("Eagle", 3);
        
        System.out.println("\nAfter creation:");
        Bird.displayConfig();
    }
}

Static blocks run when the class loads, before any objects are created. Used for complex initialization. Multiple blocks run in order. Instance blocks run before constructors.

6.5 Inheritance

extends

Inheritance allows a class to inherit fields and methods from a parent class.

The child class extends the parent class using extends. Inherited members include public and protected fields/methods. Constructors are not inherited. Use super to access parent members.

Code Example

// Parent class
class Animal {
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Animal constructor: " + name);
    }
    
    public void eat() {
        System.out.println(name + " is eating");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
    
    public void display() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}

// Child class inheriting from Animal
class Bird extends Animal {
    private double wingSpan;
    private boolean canFly;
    
    public Bird(String name, int age, double wingSpan, boolean canFly) {
        super(name, age);  // Call parent constructor
        this.wingSpan = wingSpan;
        this.canFly = canFly;
        System.out.println("Bird constructor: " + name);
    }
    
    // Additional method
    public void fly() {
        if (canFly) {
            System.out.println(name + " is flying with wingspan " + wingSpan);
        } else {
            System.out.println(name + " cannot fly");
        }
    }
    
    // Override parent method
    @Override
    public void display() {
        super.display();  // Call parent display
        System.out.println("Wingspan: " + wingSpan);
        System.out.println("Can fly: " + canFly);
    }
}

// Another child class
class Eagle extends Bird {
    private String type;
    
    public Eagle(String name, int age, double wingSpan, String type) {
        super(name, age, wingSpan, true);
        this.type = type;
    }
    
    @Override
    public void fly() {
        System.out.println(name + " the eagle soars high!");
    }
}

public class InheritanceExample {
    public static void main(String[] args) {
        System.out.println("=== Creating Bird ===");
        Bird sparrow = new Bird("Sparrow", 2, 0.25, true);
        sparrow.eat();  // Inherited from Animal
        sparrow.sleep();  // Inherited from Animal
        sparrow.fly();  // Own method
        sparrow.display();
        
        System.out.println("\n=== Creating Eagle ===");
        Eagle eagle = new Eagle("Golden Eagle", 5, 2.3, "Golden");
        eagle.eat();  // Inherited
        eagle.fly();  // Overridden
        eagle.display();
    }
}

Child class inherits from parent using extends. super calls parent constructor/methods. Override methods to change behavior. Inheritance promotes code reuse.

super

The super keyword refers to the parent class object.

super() calls the parent constructor. super.method() calls a parent method. super.field accesses a parent field.A constructor invocation using this() or super() must appear as the first statement inside the constructor.

Code Example

class Animal {
    protected String type;
    protected int age;
    
    public Animal(String type) {
        this.type = type;
        System.out.println("Animal constructor: " + type);
    }
    
    public Animal(String type, int age) {
        this.type = type;
        this.age = age;
        System.out.println("Animal constructor with age: " + type);
    }
    
    public void makeSound() {
        System.out.println(type + " makes a sound");
    }
}

class Bird extends Animal {
    private String species;
    private int count;
    
    // Constructor using super()
    public Bird(String type, String species) {
        super(type);  // Call parent constructor
        this.species = species;
        System.out.println("Bird constructor: " + species);
    }
    
    // Constructor using super() with two params
    public Bird(String type, int age, String species) {
        super(type, age);  // Call parent with age
        this.species = species;
        System.out.println("Bird constructor with age: " + species);
    }
    
    // Method using super.method()
    @Override
    public void makeSound() {
        super.makeSound();  // Call parent method
        System.out.println(species + " chirps!");
    }
    
    public void display() {
        System.out.println("Type: " + super.type);  // Access parent field
        System.out.println("Species: " + species);
        System.out.println("Age: " + super.age);
    }
}

public class SuperKeyword {
    public static void main(String[] args) {
        System.out.println("=== Bird with one param ===");
        Bird sparrow = new Bird("Aves", "Sparrow");
        sparrow.makeSound();
        sparrow.display();
        
        System.out.println("\n=== Bird with two params ===");
        Bird eagle = new Bird("Aves", 5, "Eagle");
        eagle.makeSound();
        eagle.display();
    }
}

super() calls the parent constructor. super.method() calls the parent method. super.field accesses parent fields. super must be the first statement in a constructor.

Method Overriding

Method overriding allows a subclass to provide a specific implementation of a method defined in the parent class.

Override replaces parent method functionality. The method signature must match. @Override annotation is optional but recommended. Overriding enables polymorphism.

Code Example

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
    
    public String getType() {
        return "Animal";
    }
    
    public void move() {
        System.out.println("Animal moves");
    }
}

class Bird extends Animal {
    // Override makeSound
    @Override
    public void makeSound() {
        System.out.println("Bird chirps!");
    }
    
    // Override getType
    @Override
    public String getType() {
        return "Bird";
    }
    
    // Not overriding - new method
    public void fly() {
        System.out.println("Bird flies");
    }
}

class Eagle extends Bird {
    // Override makeSound again
    @Override
    public void makeSound() {
        System.out.println("Eagle screeches!");
    }
    
    // Override move
    @Override
    public void move() {
        System.out.println("Eagle soars");
    }
    
    // Cannot override final methods
    // final void finalMethod() { }
    // class Sub extends Parent { 
    //     void finalMethod() { }  // ERROR
    // }
}

public class OverridingExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Bird bird = new Bird();
        Eagle eagle = new Eagle();
        
        System.out.println("=== Animal ===");
        animal.makeSound();
        System.out.println("Type: " + animal.getType());
        
        System.out.println("\n=== Bird ===");
        bird.makeSound();
        System.out.println("Type: " + bird.getType());
        bird.fly();
        
        System.out.println("\n=== Eagle ===");
        eagle.makeSound();
        System.out.println("Type: " + eagle.getType());
        eagle.move();
        eagle.fly();  // Inherited from Bird
        
        // Polymorphism
        Animal poly = new Eagle();
        System.out.println("\n=== Polymorphism ===");
        poly.makeSound();  // Calls Eagle's version
    }
}

Override methods to change behavior. @Override helps catch errors. The method signature must match exactly. Overriding enables polymorphism and runtime behavior changes.

6.6 Polymorphism

Compile-time Polymorphism (Overloading)

Compile-time polymorphism is resolved at compile time through method overloading.

Overloading determines which method to call based on arguments. It’s resolved at compile time. Also called static polymorphism.

Code Example

class Bird {
    // Overloaded methods
    public void display() {
        System.out.println("Display with no parameters");
    }
    
    public void display(String species) {
        System.out.println("Display with species: " + species);
    }
    
    public void display(String species, int count) {
        System.out.println("Display with species and count: " + species + ", " + count);
    }
    
    public void display(int count, String species) {
        System.out.println("Display with count and species: " + count + ", " + species);
    }
    
    public void display(double weight) {
        System.out.println("Display with weight: " + weight);
    }
}

public class CompileTimePolymorphism {
    public static void main(String[] args) {
        Bird bird = new Bird();
        
        // Compiler determines which method to call
        bird.display();
        bird.display("Sparrow");
        bird.display("Eagle", 3);
        bird.display(5, "Hawks");
        bird.display(25.5);
        
        // Different method signatures
        // Return type alone doesn't overload
        // public void method() { }
        // public int method() { }  // ERROR
    }
}

Method overloading is resolved at compile time. The compiler selects the best match based on arguments. Overloading provides flexibility and intuitive naming.

Runtime Polymorphism (Overriding)

Runtime polymorphism determines which method to call at runtime based on the actual object type.

Overriding is resolved at runtime. The JVM calls the actual object’s method. Dynamic method dispatch enables flexible behavior.

Code Example

// Base class
class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }
    
    public String getType() {
        return "Animal";
    }
}

// Subclasses
class Bird extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Chirp chirp!");
    }
    
    @Override
    public String getType() {
        return "Bird";
    }
    
    public void fly() {
        System.out.println("Flying");
    }
}

class Eagle extends Bird {
    @Override
    public void makeSound() {
        System.out.println("SCREECH!");
    }
    
    @Override
    public String getType() {
        return "Eagle";
    }
    
    public void hunt() {
        System.out.println("Hunting");
    }
}

class Sparrow extends Bird {
    @Override
    public void makeSound() {
        System.out.println("Chirp!");
    }
}

public class RuntimePolymorphism {
    public static void main(String[] args) {
        // Polymorphic references
        Animal animal1 = new Bird();
        Animal animal2 = new Eagle();
        Animal animal3 = new Sparrow();
        
        // Runtime method dispatch
        System.out.println("=== Animal as Bird ===");
        animal1.makeSound();  // Calls Bird's version
        System.out.println("Type: " + animal1.getType());
        
        System.out.println("\n=== Animal as Eagle ===");
        animal2.makeSound();  // Calls Eagle's version
        System.out.println("Type: " + animal2.getType());
        
        System.out.println("\n=== Animal as Sparrow ===");
        animal3.makeSound();  // Calls Sparrow's version
        System.out.println("Type: " + animal3.getType());
        
        // Instanceof check for safe casting
        System.out.println("\n=== Checking types ===");
        System.out.println("animal1 is Bird: " + (animal1 instanceof Bird));
        System.out.println("animal2 is Eagle: " + (animal2 instanceof Eagle));
        System.out.println("animal3 is Sparrow: " + (animal3 instanceof Sparrow));
        
        // Casting to access specific methods
        if (animal2 instanceof Eagle) {
            ((Eagle) animal2).hunt();  // Access Eagle-specific method
        }
    }
}

Runtime polymorphism uses virtual method invocation. The JVM calls the actual object’s method, not the reference type’s method. This enables flexible, extensible code.

6.7 Encapsulation & Abstraction

Access Modifiers

Access modifiers determine which parts of a program can access classes, methods, fields, and other members.

public accessible everywhere. private only in the same class. protected in same package and subclasses. Default (no modifier) in same package. Use private for encapsulation.

Code Example

package bird;

public class Bird {
    // Private - only accessible in this class
    private String species;
    private int count;
    
    // Protected - accessible in package and subclasses
    protected double weight;
    
    // Default (package-private) - accessible in same package
    String habitat;
    
    // Public - accessible everywhere
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    // Public methods provide controlled access
    public String getSpecies() { return species; }
    
    public int getCount() { return count; }
    
    public void setCount(int count) {
        if (count >= 0) {  // Validation
            this.count = count;
        } else {
            System.out.println("Invalid count!");
        }
    }
    
    // Private method - only used internally
    private void validateWeight(double weight) {
        if (weight < 0) {
            throw new IllegalArgumentException("Weight cannot be negative");
        }
    }
    
    // Protected method - accessible to subclasses
    protected void setWeight(double weight) {
        validateWeight(weight);
        this.weight = weight;
    }
    
    // Public method
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
    }
}

// In the same package
class EncapsulationExample {
    public static void main(String[] args) {
        Bird sparrow = new Bird("Sparrow", 10);
        
        // Public members accessible
        System.out.println("Species: " + sparrow.getSpecies());
        sparrow.setCount(15);
        
        // Private not accessible
        // sparrow.species = "Eagle";  // ERROR
        // sparrow.count = 5;          // ERROR
        
        // Protected accessible in same package
        sparrow.weight = 25.5;
        sparrow.setWeight(30.0);
        
        // Default accessible in same package
        sparrow.habitat = "Forest";
        
        sparrow.display();
    }
}

Access modifiers control visibility. private provides the strongest encapsulation. Getters and setters provide controlled access. Validation can be added in setters.

Getters and Setters

Getters and setters allow controlled access to private fields by providing methods for retrieving or updating their values.

Getters return field values. Setters update field values with validation. Follow JavaBeans naming convention: getX() and setX(). Encapsulation protects data integrity.

Code Example

class Bird {
    // Private fields
    private String species;
    private int count;
    private double weight;
    private boolean isMigratory;
    private String color;
    
    // Constructor
    public Bird(String species, int count, double weight) {
        this.species = species;
        this.count = count;
        this.weight = weight;
    }
    
    // Getters (read-only access)
    public String getSpecies() {
        return species;
    }
    
    public int getCount() {
        return count;
    }
    
    public double getWeight() {
        return weight;
    }
    
    public boolean isMigratory() {
        return isMigratory;
    }
    
    public String getColor() {
        return color;
    }
    
    // Setters with validation
    public void setCount(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("Count cannot be negative");
        }
        this.count = count;
        System.out.println("Count updated to: " + count);
    }
    
    public void setWeight(double weight) {
        if (weight <= 0) {
            throw new IllegalArgumentException("Weight must be positive");
        }
        this.weight = weight;
        System.out.println("Weight updated to: " + weight);
    }
    
    public void setMigratory(boolean migratory) {
        this.isMigratory = migratory;
        System.out.println("Migratory status: " + migratory);
    }
    
    public void setColor(String color) {
        if (color == null || color.isEmpty()) {
            throw new IllegalArgumentException("Color cannot be empty");
        }
        this.color = color;
        System.out.println("Color set to: " + color);
    }
    
    // Method chaining with setters
    public Bird withCount(int count) {
        setCount(count);
        return this;
    }
    
    public Bird withWeight(double weight) {
        setWeight(weight);
        return this;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight);
        System.out.println("Migratory: " + isMigratory);
        System.out.println("Color: " + color);
    }
}

public class GettersSetters {
    public static void main(String[] args) {
        Bird sparrow = new Bird("Sparrow", 10, 25.5);
        
        // Using getters
        System.out.println("Initial values:");
        System.out.println("Species: " + sparrow.getSpecies());
        System.out.println("Count: " + sparrow.getCount());
        System.out.println("Weight: " + sparrow.getWeight());
        
        // Using setters with validation
        System.out.println("\nUpdating values:");
        sparrow.setCount(15);
        sparrow.setWeight(30.0);
        sparrow.setMigratory(true);
        sparrow.setColor("Brown");
        
        System.out.println("\nFinal values:");
        sparrow.display();
    }
}

Getters provide read-only access. Setters allow controlled modification with validation. Encapsulation protects data integrity. Method chaining enables fluent-style programming.

Abstract Classes and Interfaces

Abstract classes and interfaces define contracts for subclasses. They enable abstraction and polymorphism.

Abstract classes can have both abstract and concrete methods. They provide partial implementation. Interfaces define contracts with abstract methods. Java 8+ interfaces can have default and static methods. Classes can implement multiple interfaces but extend only one abstract class.

Code Example

// ---- ABSTRACT CLASS ----
abstract class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    // Abstract method - no implementation
    public abstract void makeSound();
    
    // Concrete method - provided implementation
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
    
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// ---- INTERFACE ----
interface Flyable {
    void fly();  // Abstract method
    
    // Default method (Java 8+)
    default void glide() {
        System.out.println("Gliding through the air");
    }
    
    // Static method (Java 8+)
    static void showWings() {
        System.out.println("All birds have wings");
    }
}

interface Swimmable {
    void swim();
}

// ---- IMPLEMENTING ABSTRACT CLASS AND INTERFACES ----
class Bird extends Animal implements Flyable {
    public Bird(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " chirps!");
    }
    
    @Override
    public void fly() {
        System.out.println(name + " is flying");
    }
}

class Penguin extends Animal implements Swimmable {
    public Penguin(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " honks!");
    }
    
    @Override
    public void swim() {
        System.out.println(name + " is swimming");
    }
}

class Duck extends Animal implements Flyable, Swimmable {
    public Duck(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " quacks!");
    }
    
    @Override
    public void fly() {
        System.out.println(name + " is flying");
    }
    
    @Override
    public void swim() {
        System.out.println(name + " is swimming");
    }
}

public class AbstractAndInterface {
    public static void main(String[] args) {
        // Abstract class with concrete implementation
        Bird sparrow = new Bird("Sparrow");
        sparrow.makeSound();
        sparrow.sleep();
        sparrow.fly();
        sparrow.glide();
        Flyable.showWings();
        
        System.out.println();
        
        Penguin penguin = new Penguin("Penguin");
        penguin.makeSound();
        penguin.sleep();
        penguin.swim();
        
        System.out.println();
        
        Duck duck = new Duck("Duck");
        duck.makeSound();
        duck.fly();
        duck.swim();
        duck.glide();
        
        // Polymorphism with interfaces
        System.out.println("\n=== Polymorphism ===");
        Flyable flying = new Bird("Eagle");
        flying.fly();
        flying.glide();
        
        Swimmable swimming = new Penguin("Emperor");
        swimming.swim();
    }
}

Abstract classes provide partial implementation. Interfaces define contracts. Classes can implement multiple interfaces. Abstract classes cannot be instantiated. Abstract methods must be implemented by concrete subclasses.

6.8 Interfaces & Abstract Classes

Interfaces

Interfaces define a contract that implementing classes must follow.

An interface declares abstract methods. It can also have default, static, and private methods (Java 8+). A class implements multiple interfaces. Interfaces enable polymorphism and loose coupling.

Code Example

// ---- BASIC INTERFACE ----
interface BirdBehavior {
    // Abstract methods (implicitly public abstract)
    void eat();
    void sleep();
    
    // Default method (Java 8+)
    default void chirp() {
        System.out.println("Chirp chirp!");
    }
    
    // Static method (Java 8+)
    static void describe() {
        System.out.println("Birds are warm-blooded vertebrates");
    }
}

// ---- FUNCTIONAL INTERFACE (Single Abstract Method) ----
@FunctionalInterface
interface BirdAction {
    void perform();  // Single abstract method
}

// ---- IMPLEMENTING INTERFACE ----
class Sparrow implements BirdBehavior {
    @Override
    public void eat() {
        System.out.println("Sparrow eating seeds");
    }
    
    @Override
    public void sleep() {
        System.out.println("Sparrow sleeping in nest");
    }
    
    // Override default method
    @Override
    public void chirp() {
        System.out.println("Sparrow chirps: Tweet tweet!");
    }
}

class Eagle implements BirdBehavior {
    @Override
    public void eat() {
        System.out.println("Eagle eating meat");
    }
    
    @Override
    public void sleep() {
        System.out.println("Eagle sleeping on cliff");
    }
}

// ---- MULTIPLE INTERFACES ----
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Duck implements BirdBehavior, Flyable, Swimmable {
    @Override
    public void eat() {
        System.out.println("Duck eating plants");
    }
    
    @Override
    public void sleep() {
        System.out.println("Duck sleeping on water");
    }
    
    @Override
    public void fly() {
        System.out.println("Duck flying");
    }
    
    @Override
    public void swim() {
        System.out.println("Duck swimming");
    }
}

public class InterfaceExample {
    public static void main(String[] args) {
        // Creating objects
        Sparrow sparrow = new Sparrow();
        Eagle eagle = new Eagle();
        Duck duck = new Duck();
        
        // Using interfaces
        System.out.println("=== Sparrow ===");
        sparrow.eat();
        sparrow.sleep();
        sparrow.chirp();
        
        System.out.println("\n=== Eagle ===");
        eagle.eat();
        eagle.sleep();
        eagle.chirp();  // Uses default
        
        System.out.println("\n=== Duck ===");
        duck.eat();
        duck.sleep();
        duck.fly();
        duck.swim();
        duck.chirp();
        
        // Static method
        BirdBehavior.describe();
        
        // Polymorphism with interfaces
        System.out.println("\n=== Polymorphism ===");
        BirdBehavior bird = new Sparrow();
        bird.eat();
        bird.chirp();
        
        Flyable flyer = new Duck();
        flyer.fly();
        
        // Functional interface with lambda
        BirdAction action = () -> System.out.println("Bird action performed");
        action.perform();
    }
}

Interfaces define method signatures. Implementing classes provide implementations. Classes can implement multiple interfaces. Interfaces enable polymorphism and loose coupling.

Abstract Classes

Abstract classes are partial implementations that cannot be instantiated.

An abstract class can have both abstract and concrete methods. It provides a base for subclasses. Subclasses must implement abstract methods. Abstract classes can define shared behavior and common functionality that derived classes can inherit and extend.

Code Example

// ---- ABSTRACT CLASS ----
abstract class Bird {
    protected String name;
    protected int age;
    
    // Constructor
    public Bird(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Abstract method - must be implemented
    public abstract void makeSound();
    
    public abstract void move();
    
    // Concrete methods - shared by all subclasses
    public void eat() {
        System.out.println(name + " is eating");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
    
    public void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}

// ---- CONCRETE SUBCLASSES ----
class Sparrow extends Bird {
    public Sparrow(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " chirps: Tweet tweet!");
    }
    
    @Override
    public void move() {
        System.out.println(name + " hops and flutters");
    }
}

class Eagle extends Bird {
    private double wingSpan;
    
    public Eagle(String name, int age, double wingSpan) {
        super(name, age);
        this.wingSpan = wingSpan;
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " screeches!");
    }
    
    @Override
    public void move() {
        System.out.println(name + " soars through the air");
    }
    
    // Additional method
    public void hunt() {
        System.out.println(name + " is hunting");
    }
}

class Penguin extends Bird {
    public Penguin(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " honks!");
    }
    
    @Override
    public void move() {
        System.out.println(name + " waddles and swims");
    }
}

public class AbstractClassExample {
    public static void main(String[] args) {
        // Abstract class cannot be instantiated
        // Bird bird = new Bird("Test", 1);  // ERROR
        
        // Create concrete objects
        Sparrow sparrow = new Sparrow("Sparrow", 2);
        Eagle eagle = new Eagle("Eagle", 5, 2.3);
        Penguin penguin = new Penguin("Penguin", 3);
        
        // Using objects
        System.out.println("=== Sparrow ===");
        sparrow.displayInfo();
        sparrow.makeSound();
        sparrow.move();
        sparrow.eat();
        
        System.out.println("\n=== Eagle ===");
        eagle.displayInfo();
        eagle.makeSound();
        eagle.move();
        eagle.hunt();
        
        System.out.println("\n=== Penguin ===");
        penguin.displayInfo();
        penguin.makeSound();
        penguin.move();
        
        // Polymorphism with abstract class
        System.out.println("\n=== Polymorphism ===");
        Bird bird = new Eagle("Golden Eagle", 3, 2.5);
        bird.makeSound();
        bird.move();
    }
}

Abstract classes provide partial implementation. They cannot be instantiated. Subclasses must implement abstract methods. Abstract classes are used for common functionality and code reuse.

6.9 Advanced OOP Concepts

Inner Classes

Inner classes (non-static nested classes) are defined inside another class. They have access to outer class members.

Inner classes are non-static nested classes. They can access all members of the outer class (including private). Inner class objects are associated with an outer class instance. Used for grouping related classes and implementing callbacks.

Code Example

class Bird {
    private String species;
    private int count;
    
    // Inner class (non-static)
    class Nest {
        private int eggs;
        private String material;
        
        public Nest(int eggs, String material) {
            this.eggs = eggs;
            this.material = material;
        }
        
        public void displayNest() {
            // Inner class can access outer class members
            System.out.println("Species: " + species);
            System.out.println("Count: " + count);
            System.out.println("Eggs: " + eggs);
            System.out.println("Material: " + material);
        }
        
        public void updateCount(int newCount) {
            // Can modify outer class members
            count = newCount;
        }
    }
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    public Nest createNest(int eggs, String material) {
        return new Nest(eggs, material);
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
    }
}

public class InnerClassExample {
    public static void main(String[] args) {
        // Create outer class
        Bird sparrow = new Bird("Sparrow", 10);
        
        // Create inner class
        Bird.Nest nest = sparrow.createNest(5, "Grass");
        
        System.out.println("=== Nest Information ===");
        nest.displayNest();
        
        System.out.println("\n=== Updating Count ===");
        nest.updateCount(15);
        sparrow.display();
        
        // Alternative way to create inner class
        Bird eagle = new Bird("Eagle", 3);
        Bird.Nest eagleNest = eagle.new Nest(2, "Twigs");
        eagleNest.displayNest();
    }
}

Inner classes have access to outer class members. They are associated with an outer class instance. Useful for grouping related classes and implementing callbacks.

Static Nested Classes

Static nested classes are nested classes declared with static. They don’t have access to outer class instance members.

Static nested classes are like regular classes but defined inside another class. They cannot access non-static outer class members. They only access static members. Used for grouping related classes.

Code Example

class Bird {
    private static String category = "Aves";
    private String species;
    private int count;
    
    // Static nested class
    static class Statistics {
        private int totalBirds;
        private double averageWeight;
        
        public Statistics(int totalBirds, double averageWeight) {
            this.totalBirds = totalBirds;
            this.averageWeight = averageWeight;
        }
        
        public void displayStats() {
            // Can access static members of outer class
            System.out.println("Category: " + category);
            System.out.println("Total birds: " + totalBirds);
            System.out.println("Average weight: " + averageWeight);
        }
    }
    
    // Another static nested class
    static class Factory {
        public static Bird createSparrow() {
            return new Bird("Sparrow", 10);
        }
        
        public static Bird createEagle() {
            return new Bird("Eagle", 3);
        }
    }
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
    }
}

public class StaticNestedClass {
    public static void main(String[] args) {
        // Create static nested class without outer instance
        Bird.Statistics stats = new Bird.Statistics(100, 25.5);
        stats.displayStats();
        
        System.out.println();
        
        // Use static factory methods
        Bird sparrow = Bird.Factory.createSparrow();
        Bird eagle = Bird.Factory.createEagle();
        
        System.out.println("=== Created Birds ===");
        sparrow.display();
        eagle.display();
    }
}

Static nested classes are like top-level classes but grouped inside another class. They can access only static members of the outer class. Used for grouping related functionality.

Anonymous Classes

Anonymous classes are classes declared and instantiated without giving the class a specific name, typically for creating a one-time implementation. They are defined and instantiated in a single expression.

Anonymous classes are used for one-time implementations. They extend a class or implement an interface. They are concise but have limited reuse. Used for event handlers, callbacks, and functional interfaces.

Code Example

// Interface
interface BirdAction {
    void perform();
}

// Abstract class
abstract class Bird {
    String name;
    
    public Bird(String name) {
        this.name = name;
    }
    
    public abstract void sound();
}

public class AnonymousClass {
    public static void main(String[] args) {
        // Anonymous class implementing interface
        BirdAction chirp = new BirdAction() {
            @Override
            public void perform() {
                System.out.println("Chirp chirp!");
            }
        };
        chirp.perform();
        
        // Anonymous class extending abstract class
        Bird sparrow = new Bird("Sparrow") {
            @Override
            public void sound() {
                System.out.println(name + " tweets!");
            }
        };
        sparrow.sound();
        
        // Anonymous class with additional fields
        Bird eagle = new Bird("Eagle") {
            String type = "Golden";
            
            @Override
            public void sound() {
                System.out.println(name + " screeches!");
            }
            
            public void hunt() {
                System.out.println("Eagle hunting");
            }
        };
        eagle.sound();
        // eagle.hunt();  // ERROR: Can't call extra method (not in Bird)
        
        // Anonymous class as parameter
        processAction(new BirdAction() {
            @Override
            public void perform() {
                System.out.println("Processing bird action");
            }
        });
        
        // Anonymous class with lambda (functional interface)
        BirdAction fly = () -> System.out.println("Flying");
        fly.perform();
    }
    
    public static void processAction(BirdAction action) {
        action.perform();
    }
}

Anonymous classes are created with new InterfaceOrClass() { ... }. They are one-time implementations. Useful for callbacks and functional interfaces. They can’t be reused.

instanceof

instanceof checks if an object is an instance of a specific class or interface.

instanceof returns true if the object is of the specified type. It works with classes, interfaces, and arrays. Used for type checking before casting. Pattern matching with instanceof (Java 16+) simplifies casting.

Code Example

class Animal {
    String type = "Animal";
}

class Bird extends Animal {
    String species;
    
    public Bird(String species) {
        this.species = species;
    }
}

class Eagle extends Bird {
    double wingSpan;
    
    public Eagle(String species, double wingSpan) {
        super(species);
        this.wingSpan = wingSpan;
    }
}

class Sparrow extends Bird {
    public Sparrow(String species) {
        super(species);
    }
}

public class InstanceofExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Bird bird = new Bird("Sparrow");
        Eagle eagle = new Eagle("Golden Eagle", 2.3);
        Sparrow sparrow = new Sparrow("House Sparrow");
        
        // instanceof checks
        System.out.println("=== instanceof Checks ===");
        System.out.println("animal instanceof Animal: " + (animal instanceof Animal));
        System.out.println("bird instanceof Animal: " + (bird instanceof Animal));
        System.out.println("eagle instanceof Bird: " + (eagle instanceof Bird));
        System.out.println("sparrow instanceof Bird: " + (sparrow instanceof Bird));
        System.out.println("eagle instanceof Sparrow: " + (eagle instanceof Sparrow));
        System.out.println("bird instanceof Eagle: " + (bird instanceof Eagle));
        
        // Safe casting with instanceof
        System.out.println("\n=== Safe Casting ===");
        processAnimal(animal);
        processAnimal(bird);
        processAnimal(eagle);
        processAnimal(sparrow);
        
        // Pattern matching instanceof (Java 16+)
        System.out.println("\n=== Pattern Matching ===");
        patternMatch(animal);
        patternMatch(bird);
        patternMatch(eagle);
        patternMatch(sparrow);
    }
    
    public static void processAnimal(Animal animal) {
        if (animal instanceof Eagle) {
            Eagle eagle = (Eagle) animal;  // Explicit cast
            System.out.println("Eagle with wingspan: " + eagle.wingSpan);
        } else if (animal instanceof Sparrow) {
            Sparrow sparrow = (Sparrow) animal;
            System.out.println("Sparrow: " + sparrow.species);
        } else if (animal instanceof Bird) {
            Bird bird = (Bird) animal;
            System.out.println("Bird: " + bird.species);
        } else {
            System.out.println("Just an animal");
        }
    }
    
    // Pattern matching (Java 16+)
    public static void patternMatch(Animal animal) {
        if (animal instanceof Eagle e) {
            // e is automatically cast
            System.out.println("Eagle with wingspan: " + e.wingSpan);
        } else if (animal instanceof Sparrow s) {
            System.out.println("Sparrow: " + s.species);
        } else if (animal instanceof Bird b) {
            System.out.println("Bird: " + b.species);
        } else {
            System.out.println("Just an animal");
        }
    }
}

instanceof checks the runtime type of an object. It prevents ClassCastException. Pattern matching simplifies type checking and casting. Use instanceof before downcasting.

Packages and Import

Packages organize classes into namespaces. Import statements bring classes into scope.

Packages are declared with package at the top of the file. They prevent naming conflicts. Import statements bring classes from other packages into scope. Static import imports static members.

Code Example

// ---- package declaration (must be first line) ----
package bird.data;

// ---- import statements ----
import java.util.ArrayList;
import java.util.List;
import java.util.Date;
import static java.lang.Math.*;  // Static import

// ---- Class definition ----
public class BirdData {
    private String species;
    private int count;
    private Date observedDate;
    private List<String> habitats;
    
    public BirdData(String species, int count) {
        this.species = species;
        this.count = count;
        this.observedDate = new Date();
        this.habitats = new ArrayList<>();
    }
    
    public void addHabitat(String habitat) {
        habitats.add(habitat);
    }
    
    public void display() {
        // Using static import from Math
        double sqrt = sqrt(25);  // no Math. prefix needed
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Date: " + observedDate);
        System.out.println("Habitats: " + habitats);
        System.out.println("Sqrt of 25: " + sqrt);
    }
}

// ---- Different package ----
package bird.test;

// Import from different package
import bird.data.BirdData;
import java.util.*;  // Import all classes from java.util

public class TestBirdData {
    public static void main(String[] args) {
        // Using imported class
        BirdData data = new BirdData("Sparrow", 10);
        data.addHabitat("Forest");
        data.addHabitat("Fields");
        data.display();
        
        // Using imported classes
        List<String> birds = new ArrayList<>();
        birds.add("Eagle");
        birds.add("Hawk");
        birds.add("Sparrow");
        System.out.println("\nBirds: " + birds);
        
        // Without import - fully qualified name
        java.io.File file = new java.io.File("birds.txt");
    }
}

Packages organize code hierarchically. Imports allow using classes without fully qualified names. Static imports allow using static members directly. Use wildcards carefully.

Chapter 7: Exception Handling

7.1 Basics

try, catch, finally

Exception handling mechanisms to handle runtime errors gracefully.

try block contains code that might throw an exception. catch block handles specific exceptions. finally block always executes (whether exception occurs or not).Using multiple catch blocks allows a program to respond differently depending on the type of exception that occurs.

Code Example

import java.io.*;

public class TryCatchFinally {
    public static void main(String[] args) {
        // ---- BASIC TRY-CATCH ----
        try {
            int result = 10 / 0;  // Throws ArithmeticException
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Caught: " + e.getMessage());
        }
        
        // ---- MULTIPLE CATCH BLOCKS ----
        try {
            int[] birds = {1, 2, 3};
            int value = birds[5];  // Throws ArrayIndexOutOfBoundsException
            System.out.println("Value: " + value);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Array out of bounds: " + e.getMessage());
        } catch (ArithmeticException e) {
            System.out.println("Arithmetic: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("Generic exception: " + e.getMessage());
        }
        
        // ---- TRY-CATCH-FINALLY ----
        FileReader file = null;
        try {
            file = new FileReader("nonexistent.txt");  // Throws FileNotFoundException
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } finally {
            System.out.println("Finally block always executes");
            try {
                if (file != null) file.close();
            } catch (IOException e) {
                System.out.println("Error closing file: " + e.getMessage());
            }
        }
        
        // ---- NESTED TRY ----
        try {
            System.out.println("Outer try");
            try {
                int value = 10 / 0;
            } catch (ArithmeticException e) {
                System.out.println("Inner catch: " + e.getMessage());
                throw new RuntimeException("Wrapped exception", e);
            }
        } catch (RuntimeException e) {
            System.out.println("Outer catch: " + e.getMessage());
        }
        
        // ---- EXCEPTION STACK TRACE ----
        try {
            processBirdData("InvalidData");
        } catch (Exception e) {
            System.out.println("\nStack trace:");
            e.printStackTrace();
        }
    }
    
    public static void processBirdData(String data) throws Exception {
        if (data == null || data.isEmpty()) {
            throw new Exception("Invalid bird data: " + data);
        }
        System.out.println("Processing: " + data);
    }
}

try blocks contain risky code. catch blocks handle specific exceptions. finally always executes for cleanup. A program can use multiple catch blocks to handle different categories of exceptions with appropriate responses for each type.Nested try-catch handles errors at different levels.

try-with-resources

Try-with-resources automatically closes resources implementing AutoCloseable.

With try-with-resources, resources are automatically closed when execution leaves the try block, even if an exception occurs.Resources must implement AutoCloseable. Multiple resources can be declared in one try. Cleaner than explicit finally blocks.

Code Example

import java.io.*;
import java.util.*;

public class TryWithResources {
    public static void main(String[] args) {
        // ---- BASIC TRY-WITH-RESOURCES ----
        try (FileWriter writer = new FileWriter("birds.txt")) {
            writer.write("Sparrow, Eagle, Hawk\n");
            writer.write("Count: 10, 3, 5\n");
            System.out.println("File written successfully");
        } catch (IOException e) {
            System.out.println("Error writing file: " + e.getMessage());
        }
        
        // ---- READING WITH TRY-WITH-RESOURCES ----
        try (FileReader reader = new FileReader("birds.txt");
             BufferedReader bufferedReader = new BufferedReader(reader)) {
            
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println("Line: " + line);
            }
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("Error reading file: " + e.getMessage());
        }
        
        // ---- MULTIPLE RESOURCES ----
        try (FileReader reader = new FileReader("birds.txt");
             BufferedReader bufferedReader = new BufferedReader(reader)) {
            
            String content = bufferedReader.readLine();
            System.out.println("First line: " + content);
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- CUSTOM AUTO-CLOSEABLE ----
        class BirdResource implements AutoCloseable {
            private String name;
            
            public BirdResource(String name) {
                this.name = name;
                System.out.println("Opening: " + name);
            }
            
            public void process() {
                System.out.println("Processing: " + name);
            }
            
            @Override
            public void close() {
                System.out.println("Closing: " + name);
            }
        }
        
        try (BirdResource bird1 = new BirdResource("Eagle");
             BirdResource bird2 = new BirdResource("Sparrow")) {
            
            bird1.process();
            bird2.process();
        }
        // Resources closed automatically
        
        // ---- VS TRADITIONAL TRY-CATCH-FINALLY ----
        // Traditional approach (more verbose)
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("birds.txt"));
            System.out.println("Traditional: " + br.readLine());
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        } finally {
            try {
                if (br != null) br.close();
            } catch (IOException e) {
                System.out.println("Close error: " + e.getMessage());
            }
        }
    }
}

Try-with-resources automatically closes resources. Resources are declared in parentheses. Close method is called automatically. Cleaner and more readable than traditional try-catch-finally. All resources must implement AutoCloseable.

7.2 Throwing Exceptions

throw

The throw keyword explicitly throws an exception.

throw creates and throws an exception object. Used when a method detects an error. Can throw checked or unchecked exceptions. The exception propagates up the call stack.

Code Example

class Bird {
    private String species;
    private int count;
    
    public Bird(String species, int count) {
        // Validate with throw
        if (species == null || species.isEmpty()) {
            throw new IllegalArgumentException("Species cannot be null or empty");
        }
        if (count < 0) {
            throw new IllegalArgumentException("Count cannot be negative");
        }
        this.species = species;
        this.count = count;
    }
    
    public void setCount(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("Count cannot be negative: " + count);
        }
        this.count = count;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
    }
}

public class ThrowExample {
    public static void main(String[] args) {
        // ---- THROWING EXCEPTIONS ----
        try {
            Bird invalid = new Bird("", 10);  // Throws exception
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        try {
            Bird negative = new Bird("Sparrow", -5);  // Throws exception
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- THROWING IN METHOD ----
        Bird sparrow = new Bird("Sparrow", 10);
        try {
            sparrow.setCount(-3);  // Throws exception
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- THROWING CUSTOM EXCEPTIONS ----
        try {
            processBirdData(null);
        } catch (BirdException e) {
            System.out.println("BirdException: " + e.getMessage());
        }
        
        // ---- THROWING AND CATCHING ----
        try {
            validateBirdData("Eagle", 3);
            validateBirdData("", 5);  // Throws exception
        } catch (BirdException e) {
            System.out.println("Validation error: " + e.getMessage());
        }
    }
    
    public static void processBirdData(String data) throws BirdException {
        if (data == null) {
            throw new BirdException("Bird data cannot be null");
        }
        System.out.println("Processing: " + data);
    }
    
    public static void validateBirdData(String species, int count) throws BirdException {
        if (species == null || species.isEmpty()) {
            throw new BirdException("Invalid species: " + species);
        }
        if (count < 0) {
            throw new BirdException("Invalid count: " + count);
        }
        System.out.println("Valid bird: " + species + ", " + count);
    }
}

// Custom exception class
class BirdException extends Exception {
    public BirdException(String message) {
        super(message);
    }
    
    public BirdException(String message, Throwable cause) {
        super(message, cause);
    }
}

throw creates and throws an exception. It can be used inside methods to signal errors. The exception propagates up the call stack. Can throw both checked and unchecked exceptions.

throws

The throws keyword specifies that a method may pass one or more particular exceptions to the code that calls it.

throws is used in method signatures. It tells callers what exceptions to handle. Checked exceptions must be handled within the program or explicitly declared in the method’s throws clause. Multiple exceptions can be declared.

Code Example

import java.io.*;

class BirdDataManager {
    // Method declaring exceptions with throws
    public void saveBirdData(String species, int count) 
            throws IOException, FileNotFoundException {
        // Could throw IOException
        try (FileWriter writer = new FileWriter("birds.txt", true)) {
            writer.write(species + "," + count + "\n");
        }
        // IOException automatically thrown
    }
    
    // Method declaring custom exception
    public void validateBird(String species, int count) 
            throws BirdException {
        if (species == null || species.isEmpty()) {
            throw new BirdException("Invalid species");
        }
        if (count < 0) {
            throw new BirdException("Invalid count");
        }
    }
    
    // Method declaring multiple exceptions
    public void processBirdFile(String filename) 
            throws IOException, BirdException {
        if (filename == null) {
            throw new BirdException("Filename is null");
        }
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            String line = reader.readLine();
            if (line == null) {
                throw new BirdException("File is empty");
            }
            System.out.println("Read: " + line);
        }
        // IOException thrown by FileReader
    }
}

public class ThrowsExample {
    public static void main(String[] args) {
        BirdDataManager manager = new BirdDataManager();
        
        // ---- HANDLING DECLARED EXCEPTIONS ----
        System.out.println("=== Saving Bird Data ===");
        try {
            manager.saveBirdData("Sparrow", 10);
            manager.saveBirdData("Eagle", 3);
            System.out.println("Bird data saved");
        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());
        }
        
        // ---- HANDLING VALIDATION EXCEPTIONS ----
        System.out.println("\n=== Validating Bird Data ===");
        try {
            manager.validateBird("Sparrow", 10);  // OK
            manager.validateBird("", 5);  // Throws exception
        } catch (BirdException e) {
            System.out.println("Validation error: " + e.getMessage());
        }
        
        // ---- HANDLING MULTIPLE EXCEPTIONS ----
        System.out.println("\n=== Processing File ===");
        try {
            manager.processBirdFile("birds.txt");
            manager.processBirdFile(null);  // Throws exception
        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());
        } catch (BirdException e) {
            System.out.println("Bird Error: " + e.getMessage());
        }
        
        // ---- OVERRIDING WITH THROWS ----
        // When overriding, can only throw subclasses of the parent's exceptions
        BirdDataManager child = new BirdDataManager() {
            @Override
            public void saveBirdData(String species, int count) 
                    throws IOException {  // Same or subclass
                super.saveBirdData(species, count);
            }
        };
        
        System.out.println("\n=== Overriding Example ===");
        try {
            child.saveBirdData("Hawk", 5);
        } catch (IOException e) {
            System.out.println("Error in overridden method: " + e.getMessage());
        }
    }
}

throws declares exceptions a method might throw. Callers must handle or redeclare checked exceptions. Multiple exceptions can be declared. Override methods cannot add new checked exceptions.

7.3 Custom Exceptions

Custom exceptions are user-defined exception classes for specific error conditions.

Create custom exceptions by extending Exception (checked) or RuntimeException (unchecked). Provide constructors with message and cause. Use for business-specific errors. Improves code clarity and maintainability.

Code Example

// ---- CHECKED CUSTOM EXCEPTION ----
class BirdException extends Exception {
    private int errorCode;
    
    public BirdException(String message) {
        super(message);
    }
    
    public BirdException(String message, int errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BirdException(String message, Throwable cause) {
        super(message, cause);
    }
    
    public int getErrorCode() {
        return errorCode;
    }
}

// ---- UNCHECKED CUSTOM EXCEPTION ----
class BirdInvalidStateException extends RuntimeException {
    private String birdState;
    
    public BirdInvalidStateException(String message, String birdState) {
        super(message);
        this.birdState = birdState;
    }
    
    public String getBirdState() {
        return birdState;
    }
}

// ---- EXCEPTION WITH ADDITIONAL DATA ----
class BirdNotFoundException extends Exception {
    private String species;
    private int searchId;
    
    public BirdNotFoundException(String species, int searchId) {
        super("Bird not found: " + species + " (ID: " + searchId + ")");
        this.species = species;
        this.searchId = searchId;
    }
    
    public String getSpecies() {
        return species;
    }
    
    public int getSearchId() {
        return searchId;
    }
}

// ---- USING CUSTOM EXCEPTIONS ----
class BirdDatabase {
    private String[] birds = {"Sparrow", "Eagle", "Hawk", "Cardinal"};
    
    public String findBird(int id) throws BirdNotFoundException {
        if (id < 0 || id >= birds.length) {
            throw new BirdNotFoundException("Invalid ID", id);
        }
        return birds[id];
    }
    
    public void validateSpecies(String species) throws BirdException {
        if (species == null || species.isEmpty()) {
            throw new BirdException("Species cannot be null or empty", 1001);
        }
        if (species.length() < 2) {
            throw new BirdException("Species name too short", 1002);
        }
        System.out.println("Valid species: " + species);
    }
    
    public void processBird(String species) {
        if (species == null) {
            throw new BirdInvalidStateException("Bird state is null", "NULL_STATE");
        }
        System.out.println("Processing: " + species);
    }
}

public class CustomExceptions {
    public static void main(String[] args) {
        BirdDatabase db = new BirdDatabase();
        
        // ---- USING CHECKED EXCEPTION ----
        System.out.println("=== Checked Exception ===");
        try {
            db.validateSpecies("Sparrow");  // OK
            db.validateSpecies("");  // Throws exception
        } catch (BirdException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Error Code: " + e.getErrorCode());
        }
        
        // ---- USING EXCEPTION WITH ADDITIONAL DATA ----
        System.out.println("\n=== Exception with Additional Data ===");
        try {
            System.out.println("Bird 0: " + db.findBird(0));
            System.out.println("Bird 5: " + db.findBird(5));  // Throws exception
        } catch (BirdNotFoundException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Species: " + e.getSpecies());
            System.out.println("Search ID: " + e.getSearchId());
        }
        
        // ---- USING UNCHECKED EXCEPTION ----
        System.out.println("\n=== Unchecked Exception ===");
        try {
            db.processBird("Eagle");  // OK
            db.processBird(null);  // Throws exception
        } catch (BirdInvalidStateException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Bird State: " + e.getBirdState());
        }
        
        // ---- CATCHING SPECIFIC EXCEPTIONS ----
        System.out.println("\n=== Catching Specific Exceptions ===");
        try {
            db.validateSpecies("A");
        } catch (BirdException e) {
            System.out.println("Caught BirdException: " + e.getMessage());
            if (e.getErrorCode() == 1002) {
                System.out.println("Species name too short");
            }
        }
    }
}

Custom exceptions extend Exception or RuntimeException. They can include additional data. Provide meaningful constructors. Use for domain-specific errors. Improves error handling clarity.

7.4 Checked vs Unchecked Exceptions

Checked exceptions must be handled or declared. Unchecked exceptions are optional to handle.

Checked exceptions are checked at compile time. They must be handled or declared with throws. Include IOException, SQLException, and custom exceptions extending Exception. Unchecked exceptions are exceptions that the Java compiler does not require you to handle or declare explicitly at compile time. Include RuntimeException and its subclasses like NullPointerException, IllegalArgumentException. Errors are serious issues like OutOfMemoryError.

Code Example

import java.io.*;

// ---- CHECKED EXCEPTION ----
class CheckedExample {
    public void readFile(String filename) throws IOException {
        // IOException is checked - must be handled or declared
        try (FileReader reader = new FileReader(filename);
             BufferedReader br = new BufferedReader(reader)) {
            String line = br.readLine();
            System.out.println("Read: " + line);
        }
        // IOException automatically thrown
    }
    
    public void processData(String data) throws BirdException {
        // Custom checked exception
        if (data == null) {
            throw new BirdException("Data cannot be null");
        }
        System.out.println("Processing: " + data);
    }
}

// ---- UNCHECKED EXCEPTION ----
class UncheckedExample {
    public void divideNumbers(int a, int b) {
        // ArithmeticException is unchecked - no need to declare
        int result = a / b;  // May throw ArithmeticException
        System.out.println("Result: " + result);
    }
    
    public void accessArray(int[] array, int index) {
        // ArrayIndexOutOfBoundsException is unchecked
        int value = array[index];  // May throw exception
        System.out.println("Value: " + value);
    }
    
    public void processBird(String species) {
        // NullPointerException is unchecked
        if (species.equals("Eagle")) {  // May throw NullPointerException
            System.out.println("Processing eagle");
        }
    }
}

public class CheckedUnchecked {
    public static void main(String[] args) {
        CheckedExample checked = new CheckedExample();
        UncheckedExample unchecked = new UncheckedExample();
        
        // ---- CHECKED EXCEPTIONS ----
        System.out.println("=== Checked Exceptions ===");
        
        // Must handle checked exceptions
        try {
            checked.readFile("birds.txt");
        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());
        }
        
        try {
            checked.processData("Sparrow");
            checked.processData(null);  // Throws exception
        } catch (BirdException e) {
            System.out.println("Bird Error: " + e.getMessage());
        }
        
        // ---- UNCHECKED EXCEPTIONS ----
        System.out.println("\n=== Unchecked Exceptions ===");
        
        // Unchecked exceptions can be caught but are not required
        try {
            unchecked.divideNumbers(10, 0);  // Throws ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("Arithmetic Error: " + e.getMessage());
        }
        
        try {
            int[] array = {1, 2, 3};
            unchecked.accessArray(array, 5);  // Throws ArrayIndexOutOfBoundsException
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Array Error: " + e.getMessage());
        }
        
        try {
            unchecked.processBird(null);  // Throws NullPointerException
        } catch (NullPointerException e) {
            System.out.println("Null Error: " + e.getMessage());
        }
        
        // ---- BEST PRACTICES ----
        System.out.println("\n=== Best Practices ===");
        System.out.println("Checked exceptions: Use for recoverable conditions");
        System.out.println("Unchecked exceptions: Use for programming errors");
        System.out.println("Don't catch RuntimeException unnecessarily");
        System.out.println("Always document thrown exceptions");
    }
}

Checked exceptions must be handled or declared. Unchecked exceptions are optional to handle. Checked exceptions are for recoverable conditions. Unchecked exceptions are for programming errors. Use checked exceptions for external resources.

7.5 Exception Propagation

Exception propagation is how exceptions travel up the call stack until caught.

Exceptions propagate up the call stack. If not caught, they continue to propagate until caught or the program terminates. The stack trace shows the method call chain. printStackTrace() prints the propagation path.

Code Example

import java.io.*;

class BirdHandler {
    // Method that propagates exception
    public void processBird(String species) throws BirdException {
        validateBird(species);  // May throw BirdException
        System.out.println("Processing: " + species);
    }
    
    public void validateBird(String species) throws BirdException {
        if (species == null) {
            // Exception will propagate to caller
            throw new BirdException("Species cannot be null");
        }
        if (species.isEmpty()) {
            throw new BirdException("Species cannot be empty");
        }
        System.out.println("Valid species: " + species);
    }
    
    // Method that propagates from another method
    public void handleBirdData(String data) throws BirdException {
        // Exception propagates from readBirdData
        String processed = readBirdData(data);
        System.out.println("Handled: " + processed);
    }
    
    public String readBirdData(String data) throws BirdException {
        if (data == null) {
            // Exception will propagate to handleBirdData
            throw new BirdException("Data is null");
        }
        return data.toUpperCase();
    }
    
    // Method that catches and rethrows
    public void safeProcess(String species) throws BirdException {
        try {
            processBird(species);
        } catch (BirdException e) {
            System.out.println("Caught in safeProcess: " + e.getMessage());
            // Rethrow with additional context
            throw new BirdException("Failed to process bird: " + species, e);
        }
    }
}

public class ExceptionPropagation {
    public static void main(String[] args) {
        BirdHandler handler = new BirdHandler();
        
        // ---- EXCEPTION PROPAGATION ----
        System.out.println("=== Exception Propagation ===");
        
        try {
            handler.processBird("Sparrow");  // OK
            handler.processBird(null);  // Throws exception, propagates to main
        } catch (BirdException e) {
            System.out.println("Main caught: " + e.getMessage());
        }
        
        // ---- PROPAGATION FROM NESTED CALLS ----
        System.out.println("\n=== Propagation from Nested Calls ===");
        
        try {
            handler.handleBirdData("Eagle");  // OK
            handler.handleBirdData(null);  // Throws exception, propagates through multiple methods
        } catch (BirdException e) {
            System.out.println("Main caught: " + e.getMessage());
        }
        
        // ---- CATCH AND RETHROW ----
        System.out.println("\n=== Catch and Rethrow ===");
        
        try {
            handler.safeProcess("Sparrow");  // OK
            handler.safeProcess(null);  // Exception caught and rethrown
        } catch (BirdException e) {
            System.out.println("Main caught rethrown: " + e.getMessage());
            if (e.getCause() != null) {
                System.out.println("Cause: " + e.getCause().getMessage());
            }
        }
        
        // ---- STACK TRACE ----
        System.out.println("\n=== Stack Trace ===");
        
        try {
            handler.processBird("");  // Throws exception
        } catch (BirdException e) {
            System.out.println("Stack trace shows propagation path:");
            e.printStackTrace();
        }
        
        // ---- PROPAGATION WITH MULTIPLE EXCEPTIONS ----
        System.out.println("\n=== Multiple Exception Types ===");
        
        try {
            processComplexData(handler, "Valid");
            processComplexData(handler, null);
        } catch (BirdException | IllegalArgumentException e) {
            System.out.println("Caught exception: " + e.getMessage());
        }
    }
    
    public static void processComplexData(BirdHandler handler, String data) 
            throws BirdException {
        if (data == null) {
            throw new IllegalArgumentException("Data is null");
        }
        handler.handleBirdData(data);
    }
}

Exceptions propagate up the call stack. Each method can catch and rethrow. printStackTrace() shows the propagation path. Catching allows handling at the appropriate level. Rethrowing adds context to exceptions.

Chapter 8: Collections & Generics

8.1 Collection Framework Overview

List, Set, Queue, Map

The Collection Framework provides interfaces and implementations for data structures.

List ordered collection with duplicates. Set unordered collection with no duplicates. Queue FIFO structure. Map key-value pairs. The framework provides efficient data structures with consistent APIs.

Code Example

import java.util.*;

public class CollectionOverview {
    public static void main(String[] args) {
        // ---- LIST ----
        System.out.println("=== LIST ===");
        List<String> birds = new ArrayList<>();
        birds.add("Sparrow");
        birds.add("Eagle");
        birds.add("Hawk");
        birds.add("Sparrow");  // Allows duplicates
        
        System.out.println("List: " + birds);
        System.out.println("Element 2: " + birds.get(2));
        System.out.println("Size: " + birds.size());
        
        // ---- SET ----
        System.out.println("\n=== SET ===");
        Set<String> species = new HashSet<>();
        species.add("Sparrow");
        species.add("Eagle");
        species.add("Hawk");
        species.add("Sparrow");  // Duplicate ignored
        
        System.out.println("Set: " + species);
        System.out.println("Size: " + species.size());
        System.out.println("Contains Eagle: " + species.contains("Eagle"));
        
        // ---- QUEUE ----
        System.out.println("\n=== QUEUE ===");
        Queue<String> queue = new LinkedList<>();
        queue.add("Sparrow");
        queue.add("Eagle");
        queue.add("Hawk");
        
        System.out.println("Queue: " + queue);
        System.out.println("Front: " + queue.peek());
        System.out.println("Removed: " + queue.poll());
        System.out.println("After poll: " + queue);
        
        // ---- MAP ----
        System.out.println("\n=== MAP ===");
        Map<String, Integer> birdCounts = new HashMap<>();
        birdCounts.put("Sparrow", 10);
        birdCounts.put("Eagle", 3);
        birdCounts.put("Hawk", 5);
        
        System.out.println("Map: " + birdCounts);
        System.out.println("Sparrow count: " + birdCounts.get("Sparrow"));
        System.out.println("Contains Eagle: " + birdCounts.containsKey("Eagle"));
        
        // ---- COLLECTION FRAMEWORK HIERARCHY ----
        System.out.println("\n=== Collection Framework Hierarchy ===");
        System.out.println("Collection extends Iterable");
        System.out.println("  List (ordered, duplicates)");
        System.out.println("  Set (unordered, unique)");
        System.out.println("  Queue (FIFO)");
        System.out.println("Map (key-value pairs)");
    }
}

Collections provide data structures with rich APIs. List maintains order. Set ensures uniqueness. Queue supports FIFO operations. Map stores key-value pairs. The framework is extensible and efficient.

8.2 Interfaces

List (ArrayList, LinkedList)

List is an ordered collection that allows duplicates. ArrayList and LinkedList are common implementations.

ArrayList uses dynamic arrays. Fast random access, slow insertion/deletion. LinkedList uses doubly-linked lists. Slow random access, fast insertion/deletion. Choose based on usage patterns.

Code Example

import java.util.*;

public class ListExample {
    public static void main(String[] args) {
        // ---- ARRAYLIST ----
        System.out.println("=== ARRAYLIST ===");
        List<String> arrayList = new ArrayList<>();
        
        // Adding elements
        arrayList.add("Sparrow");
        arrayList.add("Eagle");
        arrayList.add("Hawk");
        arrayList.add(1, "Cardinal");  // Insert at index 1
        
        System.out.println("ArrayList: " + arrayList);
        System.out.println("Size: " + arrayList.size());
        System.out.println("Get index 2: " + arrayList.get(2));
        
        // Remove
        arrayList.remove("Eagle");
        System.out.println("After remove: " + arrayList);
        
        // Check
        System.out.println("Contains Sparrow: " + arrayList.contains("Sparrow"));
        System.out.println("Index of Hawk: " + arrayList.indexOf("Hawk"));
        
        // ---- LINKEDLIST ----
        System.out.println("\n=== LINKEDLIST ===");
        List<String> linkedList = new LinkedList<>();
        
        // Adding elements
        linkedList.add("Sparrow");
        linkedList.add("Eagle");
        linkedList.add("Hawk");
        linkedList.addFirst("Cardinal");  // LinkedList-specific
        
        System.out.println("LinkedList: " + linkedList);
        System.out.println("First: " + ((LinkedList<String>) linkedList).getFirst());
        System.out.println("Last: " + ((LinkedList<String>) linkedList).getLast());
        
        // ---- COMPARISON ----
        System.out.println("\n=== Performance Comparison ===");
        System.out.println("ArrayList:");
        System.out.println("  + Fast random access O(1)");
        System.out.println("  - Slow insertion/deletion in middle O(n)");
        System.out.println("LinkedList:");
        System.out.println("  + Fast insertion/deletion O(1)");
        System.out.println("  - Slow random access O(n)");
        
        // ---- ITERATION ----
        System.out.println("\n=== Iteration ===");
        for (String bird : arrayList) {
            System.out.print(bird + " ");
        }
        System.out.println();
    }
}

ArrayList uses dynamic arrays for fast access. LinkedList uses linked nodes for fast modifications. Choose ArrayList for random access, LinkedList for frequent insertions/deletions.

Set (HashSet, TreeSet, LinkedHashSet)

Set is a collection that contains no duplicate elements. Different implementations offer different ordering and performance.

HashSet uses hash table for O(1) operations. No ordering. TreeSet uses tree for sorted order. O(log n) operations. LinkedHashSet maintains insertion order. Choose based on ordering needs.

Code Example

import java.util.*;

public class SetExample {
    public static void main(String[] args) {
        // ---- HASHSET ----
        System.out.println("=== HASHSET ===");
        Set<String> hashSet = new HashSet<>();
        
        hashSet.add("Sparrow");
        hashSet.add("Eagle");
        hashSet.add("Hawk");
        hashSet.add("Sparrow");  // Duplicate ignored
        
        System.out.println("HashSet: " + hashSet);
        System.out.println("Size: " + hashSet.size());
        System.out.println("Contains Eagle: " + hashSet.contains("Eagle"));
        
        // ---- TREESET (Sorted) ----
        System.out.println("\n=== TREESET ===");
        Set<String> treeSet = new TreeSet<>();
        
        treeSet.add("Sparrow");
        treeSet.add("Eagle");
        treeSet.add("Hawk");
        treeSet.add("Cardinal");
        
        System.out.println("TreeSet: " + treeSet);  // Sorted
        
        // ---- LINKEDHASHSET (Insertion order) ----
        System.out.println("\n=== LINKEDHASHSET ===");
        Set<String> linkedHashSet = new LinkedHashSet<>();
        
        linkedHashSet.add("Sparrow");
        linkedHashSet.add("Eagle");
        linkedHashSet.add("Hawk");
        linkedHashSet.add("Cardinal");
        
        System.out.println("LinkedHashSet: " + linkedHashSet);  // Insertion order
        
        // ---- SET OPERATIONS ----
        System.out.println("\n=== Set Operations ===");
        Set<String> set1 = new HashSet<>();
        set1.add("Sparrow");
        set1.add("Eagle");
        set1.add("Hawk");
        
        Set<String> set2 = new HashSet<>();
        set2.add("Eagle");
        set2.add("Hawk");
        set2.add("Cardinal");
        
        // Union
        Set<String> union = new HashSet<>(set1);
        union.addAll(set2);
        System.out.println("Union: " + union);
        
        // Intersection
        Set<String> intersection = new HashSet<>(set1);
        intersection.retainAll(set2);
        System.out.println("Intersection: " + intersection);
        
        // Difference
        Set<String> difference = new HashSet<>(set1);
        difference.removeAll(set2);
        System.out.println("Difference: " + difference);
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("HashSet: O(1), no ordering");
        System.out.println("TreeSet: O(log n), sorted");
        System.out.println("LinkedHashSet: O(1), insertion order");
    }
}

HashSet uses hash codes for O(1) operations. TreeSet stores elements in sorted order. LinkedHashSet maintains insertion order. All prevent duplicates. Choose based on ordering and performance needs.

Map (HashMap, TreeMap, LinkedHashMap)

Map stores key-value pairs. Different implementations provide different ordering and performance.

HashMap uses hash table for O(1) operations. No ordering. TreeMap uses tree for sorted keys. O(log n) operations. LinkedHashMap maintains insertion order. Keys must be unique.

Code Example

import java.util.*;

public class MapExample {
    public static void main(String[] args) {
        // ---- HASHMAP ----
        System.out.println("=== HASHMAP ===");
        Map<String, Integer> hashMap = new HashMap<>();
        
        hashMap.put("Sparrow", 10);
        hashMap.put("Eagle", 3);
        hashMap.put("Hawk", 5);
        hashMap.put("Cardinal", 8);
        
        System.out.println("HashMap: " + hashMap);
        System.out.println("Size: " + hashMap.size());
        System.out.println("Sparrow: " + hashMap.get("Sparrow"));
        System.out.println("Contains Eagle: " + hashMap.containsKey("Eagle"));
        
        // ---- TREEMAP (Sorted by keys) ----
        System.out.println("\n=== TREEMAP ===");
        Map<String, Integer> treeMap = new TreeMap<>();
        
        treeMap.put("Sparrow", 10);
        treeMap.put("Eagle", 3);
        treeMap.put("Hawk", 5);
        treeMap.put("Cardinal", 8);
        
        System.out.println("TreeMap: " + treeMap);  // Sorted keys
        
        // ---- LINKEDHASHMAP (Insertion order) ----
        System.out.println("\n=== LINKEDHASHMAP ===");
        Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
        
        linkedHashMap.put("Sparrow", 10);
        linkedHashMap.put("Eagle", 3);
        linkedHashMap.put("Hawk", 5);
        linkedHashMap.put("Cardinal", 8);
        
        System.out.println("LinkedHashMap: " + linkedHashMap);  // Insertion order
        
        // ---- MAP OPERATIONS ----
        System.out.println("\n=== Map Operations ===");
        Map<String, Integer> birds = new HashMap<>();
        
        // putIfAbsent
        birds.putIfAbsent("Sparrow", 10);
        birds.putIfAbsent("Sparrow", 20);  // Won't override
        
        // computeIfAbsent
        birds.computeIfAbsent("Eagle", k -> 3);
        birds.computeIfAbsent("Hawk", k -> 5);
        
        System.out.println("After operations: " + birds);
        
        // Iteration
        System.out.println("\nIterating Map:");
        for (Map.Entry<String, Integer> entry : birds.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // Keys and Values
        System.out.println("\nKeys: " + birds.keySet());
        System.out.println("Values: " + birds.values());
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("HashMap: O(1), no ordering");
        System.out.println("TreeMap: O(log n), sorted by keys");
        System.out.println("LinkedHashMap: O(1), insertion order");
    }
}

HashMap uses hash codes for O(1) lookups. TreeMap stores keys in sorted order. LinkedHashMap maintains insertion order. All maps use unique keys. Choose based on ordering and performance needs.

8.3 Key Implementation Classes

ArrayList vs LinkedList

ArrayList and LinkedList are List implementations with different performance characteristics.

ArrayList uses array backing. Fast random access O(1). Slow insert/delete in middle O(n). LinkedList uses linked nodes. Fast insert/delete O(1). Slow random access O(n). Choose based on usage.

Code Example

import java.util.*;

public class ArrayListVsLinkedList {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        
        // ---- ADDING ELEMENTS ----
        System.out.println("=== Adding Elements ===");
        long start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            arrayList.add("Bird" + i);
        }
        long end = System.nanoTime();
        System.out.println("ArrayList add: " + (end - start) / 1000000 + "ms");
        
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            linkedList.add("Bird" + i);
        }
        end = System.nanoTime();
        System.out.println("LinkedList add: " + (end - start) / 1000000 + "ms");
        
        // ---- ACCESSING ELEMENTS ----
        System.out.println("\n=== Accessing Elements ===");
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            arrayList.get(i);
        }
        end = System.nanoTime();
        System.out.println("ArrayList get: " + (end - start) / 1000000 + "ms");
        
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            linkedList.get(i);
        }
        end = System.nanoTime();
        System.out.println("LinkedList get: " + (end - start) / 1000000 + "ms");
        
        // ---- INSERTING AT BEGINNING ----
        System.out.println("\n=== Inserting at Beginning ===");
        start = System.nanoTime();
        arrayList.add(0, "First");
        end = System.nanoTime();
        System.out.println("ArrayList insert at 0: " + (end - start) + "ns");
        
        start = System.nanoTime();
        ((LinkedList<String>) linkedList).addFirst("First");
        end = System.nanoTime();
        System.out.println("LinkedList insert at 0: " + (end - start) + "ns");
        
        // ---- WHEN TO USE ----
        System.out.println("\n=== When to Use ===");
        System.out.println("ArrayList:");
        System.out.println("  - Random access (get/set)");
        System.out.println("  - Adding/removing at end");
        System.out.println("  - Iterating through elements");
        System.out.println("LinkedList:");
        System.out.println("  - Frequent insert/delete at beginning");
        System.out.println("  - Queue/Deque operations");
        System.out.println("  - Fewer random accesses");
    }
}

ArrayList provides fast access but slow modifications. LinkedList provides fast modifications but slow access. Choose based on your usage pattern.

HashMap vs TreeMap

HashMap and TreeMap are Map implementations with different ordering and performance.

HashMap uses hash table. O(1) operations. No ordering. TreeMap uses red-black tree. O(log n) operations. Sorted by keys. Choose based on ordering needs.

Code Example

import java.util.*;

public class HashMapVsTreeMap {
    public static void main(String[] args) {
        // ---- PERFORMANCE COMPARISON ----
        Map<Integer, String> hashMap = new HashMap<>();
        Map<Integer, String> treeMap = new TreeMap<>();
        
        // ---- PUT OPERATIONS ----
        System.out.println("=== Put Operations ===");
        long start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            hashMap.put(i, "Bird" + i);
        }
        long end = System.nanoTime();
        System.out.println("HashMap put: " + (end - start) / 1000000 + "ms");
        
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            treeMap.put(i, "Bird" + i);
        }
        end = System.nanoTime();
        System.out.println("TreeMap put: " + (end - start) / 1000000 + "ms");
        
        // ---- GET OPERATIONS ----
        System.out.println("\n=== Get Operations ===");
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            hashMap.get(i);
        }
        end = System.nanoTime();
        System.out.println("HashMap get: " + (end - start) / 1000000 + "ms");
        
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            treeMap.get(i);
        }
        end = System.nanoTime();
        System.out.println("TreeMap get: " + (end - start) / 1000000 + "ms");
        
        // ---- ORDERING ----
        System.out.println("\n=== Ordering ===");
        Map<Integer, String> hashMap2 = new HashMap<>();
        Map<Integer, String> treeMap2 = new TreeMap<>();
        
        hashMap2.put(5, "Eagle");
        hashMap2.put(2, "Sparrow");
        hashMap2.put(8, "Hawk");
        hashMap2.put(1, "Cardinal");
        
        treeMap2.put(5, "Eagle");
        treeMap2.put(2, "Sparrow");
        treeMap2.put(8, "Hawk");
        treeMap2.put(1, "Cardinal");
        
        System.out.println("HashMap: " + hashMap2);  // No order
        System.out.println("TreeMap: " + treeMap2);  // Sorted by keys
        
        // ---- LINKEDHASHMAP (Insertion Order) ----
        System.out.println("\n=== LinkedHashMap ===");
        Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put(5, "Eagle");
        linkedHashMap.put(2, "Sparrow");
        linkedHashMap.put(8, "Hawk");
        linkedHashMap.put(1, "Cardinal");
        
        System.out.println("LinkedHashMap: " + linkedHashMap);  // Insertion order
        
        // ---- WHEN TO USE ----
        System.out.println("\n=== When to Use ===");
        System.out.println("HashMap: Fast operations, no ordering");
        System.out.println("TreeMap: Sorted keys, range queries");
        System.out.println("LinkedHashMap: Insertion order");
    }
}

HashMap provides fastest operations but no ordering. TreeMap provides sorted keys but slower. LinkedHashMap maintains insertion order. Choose based on requirements.

8.4 Iterators & Loops

Iterator

Iterator allows traversing collections safely, including removal of elements.

Iterator provides hasNext(), next(), and remove(). It’s fail-fast (throws if collection modified). Used for safe traversal and removal.

Code Example

import java.util.*;

public class IteratorExample {
    public static void main(String[] args) {
        List<String> birds = new ArrayList<>();
        birds.add("Sparrow");
        birds.add("Eagle");
        birds.add("Hawk");
        birds.add("Cardinal");
        birds.add("Finch");
        
        System.out.println("Original: " + birds);
        
        // ---- BASIC ITERATOR ----
        System.out.println("\n=== Basic Iterator ===");
        Iterator<String> iterator = birds.iterator();
        while (iterator.hasNext()) {
            String bird = iterator.next();
            System.out.println("Bird: " + bird);
        }
        
        // ---- REMOVING WITH ITERATOR ----
        System.out.println("\n=== Removing with Iterator ===");
        Iterator<String> removeIterator = birds.iterator();
        while (removeIterator.hasNext()) {
            String bird = removeIterator.next();
            if (bird.equals("Eagle")) {
                removeIterator.remove();  // Safe removal
                System.out.println("Removed: " + bird);
            }
        }
        System.out.println("After removal: " + birds);
        
        // ---- ITERATOR WITH SET ----
        System.out.println("\n=== Iterator with Set ===");
        Set<String> birdSet = new HashSet<>(birds);
        Iterator<String> setIterator = birdSet.iterator();
        while (setIterator.hasNext()) {
            String bird = setIterator.next();
            System.out.println("Set bird: " + bird);
        }
        
        // ---- ITERATOR WITH MAP ----
        System.out.println("\n=== Iterator with Map ===");
        Map<String, Integer> birdCounts = new HashMap<>();
        birdCounts.put("Sparrow", 10);
        birdCounts.put("Eagle", 3);
        birdCounts.put("Hawk", 5);
        
        Iterator<Map.Entry<String, Integer>> mapIterator = 
            birdCounts.entrySet().iterator();
        while (mapIterator.hasNext()) {
            Map.Entry<String, Integer> entry = mapIterator.next();
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // ---- FAIL-FAST BEHAVIOR ----
        System.out.println("\n=== Fail-Fast Iterator ===");
        List<String> failList = new ArrayList<>(birds);
        Iterator<String> failIterator = failList.iterator();
        
        try {
            while (failIterator.hasNext()) {
                String bird = failIterator.next();
                if (bird.equals("Hawk")) {
                    failList.remove("Hawk");  // Concurrent modification
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Concurrent modification detected!");
        }
        
        // ---- COMPARING ITERATION METHODS ----
        System.out.println("\n=== Comparison ===");
        System.out.println("Iterator: Safe removal, fail-fast");
        System.out.println("Enhanced for: Read-only, concise");
        System.out.println("Regular for: Indexed access");
    }
}

Iterator provides safe traversal and removal. It throws ConcurrentModificationException if the collection is modified during iteration. Use it when you need to remove elements.

ListIterator

ListIterator extends Iterator for List collections with bidirectional traversal.

ListIterator allows forward and backward iteration. Provides hasPrevious(), previous(), nextIndex(), and previousIndex(). Also supports add() and set().

Code Example

import java.util.*;

public class ListIteratorExample {
    public static void main(String[] args) {
        List<String> birds = new ArrayList<>();
        birds.add("Sparrow");
        birds.add("Eagle");
        birds.add("Hawk");
        birds.add("Cardinal");
        birds.add("Finch");
        
        System.out.println("Original: " + birds);
        
        // ---- FORWARD ITERATION ----
        System.out.println("\n=== Forward Iteration ===");
        ListIterator<String> forward = birds.listIterator();
        while (forward.hasNext()) {
            System.out.println("Forward: " + forward.next());
        }
        
        // ---- BACKWARD ITERATION ----
        System.out.println("\n=== Backward Iteration ===");
        ListIterator<String> backward = birds.listIterator(birds.size());
        while (backward.hasPrevious()) {
            System.out.println("Backward: " + backward.previous());
        }
        
        // ---- MODIFYING WITH LISTITERATOR ----
        System.out.println("\n=== Modifying with ListIterator ===");
        ListIterator<String> modifier = birds.listIterator();
        while (modifier.hasNext()) {
            String bird = modifier.next();
            if (bird.equals("Eagle")) {
                modifier.set("Golden Eagle");  // Replace
            }
            if (bird.equals("Hawk")) {
                modifier.add("Falcon");  // Insert after Hawk
            }
        }
        System.out.println("After modifications: " + birds);
        
        // ---- INDEX ACCESS ----
        System.out.println("\n=== Index Access ===");
        ListIterator<String> indexed = birds.listIterator();
        while (indexed.hasNext()) {
            int index = indexed.nextIndex();
            String bird = indexed.next();
            System.out.println("Index " + index + ": " + bird);
        }
        
        // ---- REVERSE WITH REMOVAL ----
        System.out.println("\n=== Reverse with Removal ===");
        ListIterator<String> remover = birds.listIterator(birds.size());
        while (remover.hasPrevious()) {
            String bird = remover.previous();
            if (bird.equals("Finch")) {
                remover.remove();
                System.out.println("Removed: " + bird);
            }
        }
        System.out.println("After removal: " + birds);
        
        // ---- COMPARING ITERATORS ----
        System.out.println("\n=== Comparison ===");
        System.out.println("Iterator: Forward only, remove only");
        System.out.println("ListIterator: Bidirectional, add, set, remove, indices");
    }
}

ListIterator extends Iterator with bidirectional traversal. Supports add, set, and remove operations. Provides index information. Only available for List collections.

Enhanced For-Loop

Enhanced for-loop (for-each) provides concise iteration over arrays and collections.

Syntax: for (Type variable : collection). Works with arrays and Iterable implementations. Read-only iteration (cannot modify collection). No index access.

Code Example

import java.util.*;

public class EnhancedForLoop {
    public static void main(String[] args) {
        // ---- ARRAY ----
        System.out.println("=== Array ===");
        String[] birdArray = {"Sparrow", "Eagle", "Hawk", "Cardinal"};
        for (String bird : birdArray) {
            System.out.println("Bird: " + bird);
        }
        
        // ---- LIST ----
        System.out.println("\n=== List ===");
        List<String> birdList = new ArrayList<>();
        birdList.add("Sparrow");
        birdList.add("Eagle");
        birdList.add("Hawk");
        birdList.add("Cardinal");
        
        for (String bird : birdList) {
            System.out.println("Bird: " + bird);
        }
        
        // ---- SET ----
        System.out.println("\n=== Set ===");
        Set<String> birdSet = new HashSet<>(birdList);
        for (String bird : birdSet) {
            System.out.println("Bird: " + bird);
        }
        
        // ---- MAP ----
        System.out.println("\n=== Map ===");
        Map<String, Integer> birdCounts = new HashMap<>();
        birdCounts.put("Sparrow", 10);
        birdCounts.put("Eagle", 3);
        birdCounts.put("Hawk", 5);
        
        for (Map.Entry<String, Integer> entry : birdCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // ---- CANNOT MODIFY DURING ITERATION ----
        System.out.println("\n=== Cannot Modify ===");
        List<String> modifyList = new ArrayList<>(birdList);
        try {
            for (String bird : modifyList) {
                if (bird.equals("Eagle")) {
                    modifyList.remove(bird);  // Throws ConcurrentModificationException
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Cannot modify during iteration!");
        }
        
        // ---- LIMITATIONS ----
        System.out.println("\n=== Limitations ===");
        System.out.println("1. Cannot modify collection during iteration");
        System.out.println("2. No index access");
        System.out.println("3. Read-only iteration");
        System.out.println("4. Cannot skip elements");
    }
}

Enhanced for-loop creates an iterator internally. It’s concise but read-only. Works with arrays and Iterable collections. Use it when you don’t need index or modification.

forEach() Method

forEach() is a method on Iterable collections that accepts a lambda expression.

forEach() takes a Consumer functional interface. It applies the action to each element. Supports lambda expressions. More functional-style than enhanced for-loop.

Code Example

import java.util.*;
import java.util.function.Consumer;

public class ForEachMethod {
    public static void main(String[] args) {
        List<String> birds = new ArrayList<>();
        birds.add("Sparrow");
        birds.add("Eagle");
        birds.add("Hawk");
        birds.add("Cardinal");
        
        // ---- BASIC FOREACH ----
        System.out.println("=== Basic forEach ===");
        birds.forEach(bird -> System.out.println("Bird: " + bird));
        
        // ---- WITH METHOD REFERENCE ----
        System.out.println("\n=== With Method Reference ===");
        birds.forEach(System.out::println);
        
        // ---- CONSUMER IMPLEMENTATION ----
        System.out.println("\n=== Consumer Implementation ===");
        Consumer<String> birdPrinter = bird -> {
            System.out.println("Processing: " + bird);
            System.out.println("Length: " + bird.length());
        };
        birds.forEach(birdPrinter);
        
        // ---- MAP FOREACH ----
        System.out.println("\n=== Map forEach ===");
        Map<String, Integer> birdCounts = new HashMap<>();
        birdCounts.put("Sparrow", 10);
        birdCounts.put("Eagle", 3);
        birdCounts.put("Hawk", 5);
        
        birdCounts.forEach((species, count) -> {
            System.out.println(species + ": " + count);
        });
        
        // ---- FILTERING WITH FOREACH ----
        System.out.println("\n=== Filtering ===");
        birds.forEach(bird -> {
            if (bird.startsWith("S")) {
                System.out.println("Starts with S: " + bird);
            }
        });
        
        // ---- COMPLEX OPERATIONS ----
        System.out.println("\n=== Complex Operations ===");
        birds.forEach(bird -> {
            String upper = bird.toUpperCase();
            System.out.println("Uppercase: " + upper);
        });
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("Enhanced for: Statement-based, concise");
        System.out.println("forEach: Functional, lambda, more flexible");
    }
}

forEach() applies a lambda to each element. It’s functional and concise. Works with any Iterable. Supports method references and complex operations.

8.5 Generics

Generic Classes

Generic classes use type parameters to enable type-safe operations on different types.

Generic class uses <T> type parameter. T is replaced with the actual type at compile time. Provides type safety and avoids casting.

Code Example

// ---- GENERIC CLASS ----
class BirdContainer<T> {
    private T item;
    private List<T> items = new ArrayList<>();
    
    public void add(T item) {
        this.item = item;
        items.add(item);
        System.out.println("Added: " + item);
    }
    
    public T getItem() {
        return item;
    }
    
    public List<T> getAll() {
        return new ArrayList<>(items);
    }
    
    public boolean isEmpty() {
        return items.isEmpty();
    }
}

// ---- MULTIPLE TYPE PARAMETERS ----
class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    public void display() {
        System.out.println("Key: " + key + ", Value: " + value);
    }
}

// ---- GENERIC CLASS WITH BOUNDED TYPE ----
class NumberContainer<T extends Number> {
    private T value;
    
    public NumberContainer(T value) {
        this.value = value;
    }
    
    public double getDouble() {
        return value.doubleValue();
    }
    
    public void display() {
        System.out.println("Value: " + value + ", Type: " + value.getClass());
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        // ---- BASIC GENERIC CLASS ----
        System.out.println("=== Basic Generic Class ===");
        BirdContainer<String> stringContainer = new BirdContainer<>();
        stringContainer.add("Sparrow");
        stringContainer.add("Eagle");
        System.out.println("Item: " + stringContainer.getItem());
        System.out.println("All: " + stringContainer.getAll());
        
        BirdContainer<Integer> intContainer = new BirdContainer<>();
        intContainer.add(10);
        intContainer.add(20);
        System.out.println("Item: " + intContainer.getItem());
        
        // ---- MULTIPLE TYPE PARAMETERS ----
        System.out.println("\n=== Multiple Type Parameters ===");
        Pair<String, Integer> pair = new Pair<>("Sparrow", 10);
        pair.display();
        
        Pair<Integer, String> pair2 = new Pair<>(1, "Eagle");
        pair2.display();
        
        // ---- BOUNDED TYPE ----
        System.out.println("\n=== Bounded Type ===");
        NumberContainer<Integer> intNumber = new NumberContainer<>(100);
        intNumber.display();
        System.out.println("As double: " + intNumber.getDouble());
        
        NumberContainer<Double> doubleNumber = new NumberContainer<>(25.5);
        doubleNumber.display();
        
        // ---- TYPE ERASURE ----
        System.out.println("\n=== Type Erasure ===");
        System.out.println("Generics are compile-time only");
        System.out.println("At runtime, List<String> becomes List");
        System.out.println("Type information is erased");
    }
}

Generic classes use type parameters for compile-time type safety. T is a placeholder for the actual type. Multiple type parameters are supported. Bounded types restrict the types allowed.

Generic Methods

Generic methods have type parameters independent of their class.

Generic methods specify their type parameters before the method’s return type, allowing the method to work with different data types. They can be static or instance. Type inference determines the type.

Code Example

import java.util.*;

public class GenericMethodExample {
    // Generic method with one type parameter
    public static <T> T getFirst(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }
    
    // Generic method with bounded type
    public static <T extends Number> double sum(T[] array) {
        double total = 0;
        for (T item : array) {
            total += item.doubleValue();
        }
        return total;
    }
    
    // Generic method with multiple parameters
    public static <T, U> void printPair(T first, U second) {
        System.out.println("First: " + first);
        System.out.println("Second: " + second);
    }
    
    // Generic method returning generic collection
    public static <T> List<T> toList(T[] array) {
        List<T> list = new ArrayList<>();
        for (T item : array) {
            list.add(item);
        }
        return list;
    }
    
    // Generic method with wildcard
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        // ---- BASIC GENERIC METHOD ----
        System.out.println("=== Basic Generic Method ===");
        String[] birds = {"Sparrow", "Eagle", "Hawk"};
        String first = getFirst(birds);
        System.out.println("First: " + first);
        
        Integer[] numbers = {10, 20, 30};
        Integer firstNum = getFirst(numbers);
        System.out.println("First number: " + firstNum);
        
        // ---- BOUNDED TYPE ----
        System.out.println("\n=== Bounded Type ===");
        Integer[] ints = {1, 2, 3, 4, 5};
        Double[] doubles = {1.1, 2.2, 3.3};
        
        System.out.println("Sum of ints: " + sum(ints));
        System.out.println("Sum of doubles: " + sum(doubles));
        
        // ---- MULTIPLE PARAMETERS ----
        System.out.println("\n=== Multiple Parameters ===");
        printPair("Sparrow", 10);
        printPair(42, "Eagle");
        printPair(3.14, true);
        
        // ---- TO LIST ----
        System.out.println("\n=== To List ===");
        List<String> birdList = toList(birds);
        System.out.println("List: " + birdList);
        
        // ---- WILDCARD ----
        System.out.println("\n=== Wildcard ===");
        List<String> strList = Arrays.asList("Sparrow", "Eagle");
        List<Integer> intList = Arrays.asList(1, 2, 3);
        
        printList(strList);
        printList(intList);
    }
}

Generic methods place their type parameters before the return type in the method declaration, enabling the same method to work with multiple data types. They provide type safety for individual methods. Type inference determines the actual type from the arguments.

Wildcards

Wildcards (?) provide flexibility in generic types.

? unknown type. ? extends T upper bounded wildcard (T or subtype). ? super T lower bounded wildcard (T or supertype). PECS stands for Producer Extends, Consumer Super.

Code Example

import java.util.*;

public class WildcardExample {
    // Unbounded wildcard
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
    
    // Upper bounded wildcard (extends)
    public static double sumList(List<? extends Number> list) {
        double sum = 0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
    // Lower bounded wildcard (super)
    public static void addBirds(List<? super String> list, String... birds) {
        for (String bird : birds) {
            list.add(bird);
        }
    }
    
    // Producer Extends, Consumer Super (PECS)
    public static void processList(List<?> list) {
        System.out.println("Processing: " + list.size() + " items");
    }
    
    public static void main(String[] args) {
        // ---- UNBOUNDED WILDCARD ----
        System.out.println("=== Unbounded Wildcard ===");
        List<String> strings = Arrays.asList("Sparrow", "Eagle", "Hawk");
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
        
        printList(strings);
        printList(integers);
        
        // ---- UPPER BOUNDED WILDCARD ----
        System.out.println("\n=== Upper Bounded Wildcard (extends) ===");
        List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
        List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
        List<Number> numbers = Arrays.asList(1, 2.5, 3);
        
        System.out.println("Sum ints: " + sumList(ints));
        System.out.println("Sum doubles: " + sumList(doubles));
        System.out.println("Sum numbers: " + sumList(numbers));
        
        // ---- LOWER BOUNDED WILDCARD ----
        System.out.println("\n=== Lower Bounded Wildcard (super) ===");
        List<Object> objectList = new ArrayList<>();
        List<String> stringList = new ArrayList<>();
        List<CharSequence> charSequenceList = new ArrayList<>();
        
        addBirds(objectList, "Sparrow", "Eagle");
        addBirds(stringList, "Hawk", "Cardinal");
        addBirds(charSequenceList, "Finch", "Robin");
        
        System.out.println("Object list: " + objectList);
        System.out.println("String list: " + stringList);
        System.out.println("CharSequence list: " + charSequenceList);
        
        // ---- PECS RULE ----
        System.out.println("\n=== PECS Rule (Producer Extends, Consumer Super) ===");
        System.out.println("? extends T: Used when you READ from the list (Producer)");
        System.out.println("? super T: Used when you WRITE to the list (Consumer)");
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("? : Unknown type");
        System.out.println("? extends T: T or subtype");
        System.out.println("? super T: T or supertype");
    }
}

Wildcards provide flexibility in generic types. ? is unknown type. ? extends T allows T and subtypes. ? super T allows T and supertypes. PECS defines usage patterns.

Chapter 9: File Handling & I/O

9.1 Text Streams

FileReader and BufferedReader

FileReader and BufferedReader read text data from files efficiently.

FileReader reads characters from a file. BufferedReader adds buffering for efficiency. Provides readLine() for line-by-line reading. Always close resources or use try-with-resources.

Code Example

import java.io.*;
import java.util.*;

public class FileReaderExample {
    public static void main(String[] args) {
        // ---- READING WITH FILE READER ----
        System.out.println("=== Reading with FileReader ===");
        String filename = "birds.txt";
        
        // Create file for reading
        createSampleFile(filename);
        
        // ---- BASIC FILE READER ----
        try (FileReader reader = new FileReader(filename);
             BufferedReader bufferedReader = new BufferedReader(reader)) {
            
            String line;
            System.out.println("Reading file line by line:");
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println("Line: " + line);
            }
            
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());
        }
        
        // ---- READING ENTIRE FILE ----
        System.out.println("\n=== Reading Entire File ===");
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            StringBuilder content = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
            System.out.println("File content:");
            System.out.println(content.toString());
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- PROCESSING FILE DATA ----
        System.out.println("\n=== Processing File Data ===");
        List<String> birds = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (!line.isEmpty()) {
                    birds.add(line);
                }
            }
            System.out.println("Birds read: " + birds);
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void createSampleFile(String filename) {
        try (FileWriter writer = new FileWriter(filename)) {
            writer.write("Sparrow\n");
            writer.write("Eagle\n");
            writer.write("Hawk\n");
            writer.write("Cardinal\n");
            writer.write("Finch\n");
            System.out.println("Sample file created: " + filename);
        } catch (IOException e) {
            System.out.println("Error creating file: " + e.getMessage());
        }
    }
}

FileReader opens a file for character reading. BufferedReader adds buffering for efficiency. readLine() reads entire lines. Use try-with-resources for automatic cleanup.

FileWriter and BufferedWriter

FileWriter and BufferedWriter write text data to files efficiently.

FileWriter writes characters to a file. BufferedWriter adds buffering for efficiency. PrintWriter provides convenient formatted output.

Code Example

import java.io.*;

public class FileWriterExample {
    public static void main(String[] args) {
        // ---- WRITING WITH FILE WRITER ----
        System.out.println("=== Writing with FileWriter ===");
        String filename = "bird_data.txt";
        
        // ---- BASIC FILE WRITER ----
        try (FileWriter writer = new FileWriter(filename)) {
            writer.write("Sparrow: 10\n");
            writer.write("Eagle: 3\n");
            writer.write("Hawk: 5\n");
            writer.write("Cardinal: 8\n");
            System.out.println("File written: " + filename);
            
        } catch (IOException e) {
            System.out.println("Error writing file: " + e.getMessage());
        }
        
        // ---- BUFFERED WRITER ----
        System.out.println("\n=== BufferedWriter ===");
        String outputFile = "buffered_birds.txt";
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
            writer.write("Sparrow: 10");
            writer.newLine();
            writer.write("Eagle: 3");
            writer.newLine();
            writer.write("Hawk: 5");
            writer.newLine();
            System.out.println("Buffered file written: " + outputFile);
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- PRINT WRITER ----
        System.out.println("\n=== PrintWriter ===");
        String printFile = "print_birds.txt";
        try (PrintWriter writer = new PrintWriter(new FileWriter(printFile))) {
            writer.println("Sparrow: 10");
            writer.println("Eagle: 3");
            writer.println("Hawk: 5");
            writer.printf("Cardinal: %d\n", 8);
            writer.printf("Finch: %d\n", 6);
            System.out.println("PrintWriter file written: " + printFile);
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- APPENDING TO FILE ----
        System.out.println("\n=== Appending to File ===");
        try (FileWriter writer = new FileWriter(filename, true)) {  // Append mode
            writer.write("Finch: 6\n");
            writer.write("Robin: 4\n");
            System.out.println("Data appended to file");
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- READING BACK THE FILE ----
        System.out.println("\n=== Reading Back File ===");
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            String line;
            System.out.println("File content:");
            while ((line = reader.readLine()) != null) {
                System.out.println("  " + line);
            }
        } catch (IOException e) {
            System.out.println("Error reading: " + e.getMessage());
        }
    }
}

FileWriter opens a file for writing. BufferedWriter adds buffering for performance. PrintWriter provides convenient methods like println() and printf(). Use append mode to add to existing files.

9.2 Binary Streams

FileInputStream and FileOutputStream

FileInputStream and FileOutputStream handle binary data, reading and writing raw bytes.

FileInputStream reads raw bytes from a file. FileOutputStream writes raw bytes to a file. Used for images, audio, serialized objects, and any binary data.

Code Example

import java.io.*;

public class BinaryStreamExample {
    public static void main(String[] args) {
        // ---- WRITING BINARY DATA ----
        System.out.println("=== Writing Binary Data ===");
        String filename = "binary_data.bin";
        
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            // Write integers as bytes
            int[] birds = {10, 20, 30, 40, 50};
            for (int bird : birds) {
                fos.write(bird);  // Writes least significant byte
            }
            System.out.println("Binary data written: " + filename);
            
        } catch (IOException e) {
            System.out.println("Error writing: " + e.getMessage());
        }
        
        // ---- READING BINARY DATA ----
        System.out.println("\n=== Reading Binary Data ===");
        try (FileInputStream fis = new FileInputStream(filename)) {
            int value;
            System.out.println("Reading bytes:");
            while ((value = fis.read()) != -1) {
                System.out.print(value + " ");
            }
            System.out.println();
            
        } catch (IOException e) {
            System.out.println("Error reading: " + e.getMessage());
        }
        
        // ---- WRITING BYTE ARRAY ----
        System.out.println("\n=== Writing Byte Array ===");
        String arrayFile = "array_data.bin";
        byte[] data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
        
        try (FileOutputStream fos = new FileOutputStream(arrayFile)) {
            fos.write(data);
            System.out.println("Byte array written: " + data.length + " bytes");
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- READING BYTE ARRAY ----
        System.out.println("\n=== Reading Byte Array ===");
        try (FileInputStream fis = new FileInputStream(arrayFile)) {
            byte[] buffer = new byte[20];
            int bytesRead = fis.read(buffer);
            System.out.println("Bytes read: " + bytesRead);
            System.out.print("Data: ");
            for (int i = 0; i < bytesRead; i++) {
                System.out.print(buffer[i] + " ");
            }
            System.out.println();
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- COPYING FILE ----
        System.out.println("\n=== Copying File ===");
        String source = "binary_data.bin";
        String dest = "copy_data.bin";
        
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            System.out.println("File copied: " + source + " -> " + dest);
            
        } catch (IOException e) {
            System.out.println("Error copying: " + e.getMessage());
        }
    }
}

FileInputStream reads raw bytes. FileOutputStream writes raw bytes. Read returns -1 at end of file. Use byte arrays for efficient reading/writing. Perfect for binary data like images and serialized objects.

DataInputStream and DataOutputStream

DataInputStream and DataOutputStream handle primitive types in binary format.

DataOutputStream writes primitive types in binary format. DataInputStream reads primitive types in binary format. Supports writeInt(), writeDouble(), writeUTF(), etc. Type-safe and preserves data.

Code Example

import java.io.*;

public class DataStreamExample {
    public static void main(String[] args) {
        String filename = "bird_data.dat";
        
        // ---- WRITING PRIMITIVE TYPES ----
        System.out.println("=== Writing Primitive Types ===");
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream(filename))) {
            
            // Write bird data
            dos.writeUTF("Sparrow");    // String
            dos.writeInt(10);            // Count
            dos.writeDouble(25.5);       // Weight
            dos.writeBoolean(true);      // Migratory
            
            dos.writeUTF("Eagle");
            dos.writeInt(3);
            dos.writeDouble(4500.0);
            dos.writeBoolean(false);
            
            System.out.println("Data written to: " + filename);
            
        } catch (IOException e) {
            System.out.println("Error writing: " + e.getMessage());
        }
        
        // ---- READING PRIMITIVE TYPES ----
        System.out.println("\n=== Reading Primitive Types ===");
        try (DataInputStream dis = new DataInputStream(
                new FileInputStream(filename))) {
            
            System.out.println("Reading bird data:");
            while (dis.available() > 0) {
                String species = dis.readUTF();
                int count = dis.readInt();
                double weight = dis.readDouble();
                boolean migratory = dis.readBoolean();
                
                System.out.printf("%s: %d birds, %.1f g, Migratory: %s%n",
                    species, count, weight, migratory);
            }
            
        } catch (IOException e) {
            System.out.println("Error reading: " + e.getMessage());
        }
        
        // ---- WRITING ARRAY OF DATA ----
        System.out.println("\n=== Writing Array Data ===");
        String arrayFile = "array_data.dat";
        
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream(arrayFile))) {
            
            dos.writeInt(5);  // Array size
            for (int i = 1; i <= 5; i++) {
                dos.writeInt(i * 10);
            }
            System.out.println("Array written: " + arrayFile);
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- READING ARRAY DATA ----
        System.out.println("\n=== Reading Array Data ===");
        try (DataInputStream dis = new DataInputStream(
                new FileInputStream(arrayFile))) {
            
            int size = dis.readInt();
            System.out.println("Array size: " + size);
            System.out.print("Values: ");
            for (int i = 0; i < size; i++) {
                System.out.print(dis.readInt() + " ");
            }
            System.out.println();
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- READ ORDER IMPORTANCE ----
        System.out.println("\n=== Order Matters ===");
        System.out.println("Data must be read in the same order it was written");
        System.out.println("Data types must match exactly");
    }
}

DataOutputStream writes primitive types in a portable binary format. DataInputStream reads them back. Read order must match write order. Supports strings with writeUTF() and readUTF().

9.3 Object Serialization

Serializable Interface

Serialization converts objects to byte streams for storage or transmission.

The Serializable interface marks objects as serializable. ObjectOutputStream writes serialized objects. ObjectInputStream reads serialized objects. serialVersionUID ensures version compatibility.

Code Example

import java.io.*;

// ---- SERIALIZABLE CLASS ----
class Bird implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String species;
    private int count;
    private double weight;
    private boolean migratory;
    
    // Transient fields are not serialized
    private transient String tempData;
    
    // Static fields are not serialized
    private static int totalBirds = 0;
    
    public Bird(String species, int count, double weight, boolean migratory) {
        this.species = species;
        this.count = count;
        this.weight = weight;
        this.migratory = migratory;
        this.tempData = "Temporary: " + species;
        totalBirds++;
    }
    
    public void display() {
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Weight: " + weight + "g");
        System.out.println("Migratory: " + migratory);
        System.out.println("Temp Data: " + tempData);
        System.out.println("Total Birds: " + totalBirds);
    }
    
    // Custom serialization
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();  // Write default fields
        // Add custom data if needed
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();  // Read default fields
        // Initialize transient fields
        tempData = "Restored: " + species;
    }
}

public class SerializableExample {
    public static void main(String[] args) {
        String filename = "bird.ser";
        
        // ---- SERIALIZING ----
        System.out.println("=== Serializing Object ===");
        Bird sparrow = new Bird("Sparrow", 10, 25.5, true);
        System.out.println("Before serialization:");
        sparrow.display();
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filename))) {
            oos.writeObject(sparrow);
            System.out.println("Object serialized to: " + filename);
            
        } catch (IOException e) {
            System.out.println("Error serializing: " + e.getMessage());
        }
        
        // ---- DESERIALIZING ----
        System.out.println("\n=== Deserializing Object ===");
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filename))) {
            
            Bird restored = (Bird) ois.readObject();
            System.out.println("After deserialization:");
            restored.display();
            
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Error deserializing: " + e.getMessage());
        }
        
        // ---- SERIALIZING MULTIPLE OBJECTS ----
        System.out.println("\n=== Serializing Multiple Objects ===");
        String multiFile = "birds.ser";
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(multiFile))) {
            
            oos.writeObject(new Bird("Eagle", 3, 4500.0, false));
            oos.writeObject(new Bird("Hawk", 5, 1200.0, true));
            oos.writeObject(new Bird("Cardinal", 8, 45.0, false));
            System.out.println("Multiple objects serialized");
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- DESERIALIZING MULTIPLE OBJECTS ----
        System.out.println("\n=== Deserializing Multiple Objects ===");
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(multiFile))) {
            
            Bird bird;
            while (true) {
                try {
                    bird = (Bird) ois.readObject();
                    System.out.println("Restored: " + bird.toString());
                } catch (EOFException e) {
                    break;  // End of file
                }
            }
            
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

Serialization converts objects to byte streams. transient fields are skipped. static fields are not serialized. serialVersionUID helps version compatibility. Use custom serialization for complex objects.

Transient and Static Fields

Transient fields are excluded from serialization. Static fields are associated with the class itself rather than individual objects, so all instances of that class share the same field.

transient marks fields to be skipped during serialization. Useful for sensitive data. static fields are not serialized (they belong to the class). Both help control what gets serialized.

Code Example

import java.io.*;

class BirdData implements Serializable {
    private static final long serialVersionUID = 1L;
    
    // Normal field - serialized
    private String species;
    private int count;
    
    // Transient field - not serialized
    private transient String password;
    private transient double tempWeight;
    
    // Static field - not serialized (belongs to class)
    private static int totalBirds = 0;
    
    // Static final - not serialized (constant)
    private static final String CATEGORY = "Aves";
    
    public BirdData(String species, int count, String password) {
        this.species = species;
        this.count = count;
        this.password = password;
        this.tempWeight = count * 10.0;
        totalBirds++;
    }
    
    public void display(String context) {
        System.out.println("=== " + context + " ===");
        System.out.println("Species: " + species);
        System.out.println("Count: " + count);
        System.out.println("Password: " + password);
        System.out.println("Temp Weight: " + tempWeight);
        System.out.println("Total Birds (static): " + totalBirds);
        System.out.println("Category (static final): " + CATEGORY);
        System.out.println();
    }
}

public class TransientStaticExample {
    public static void main(String[] args) {
        String filename = "bird_transient.ser";
        
        // ---- SERIALIZING ----
        System.out.println("=== Before Serialization ===");
        BirdData original = new BirdData("Sparrow", 10, "secret123");
        original.display("Original Object");
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filename))) {
            oos.writeObject(original);
            System.out.println("Object serialized");
            
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- MODIFYING STATIC ----
        // Static fields are class-level, not serialized
        BirdData.totalBirds = 100;  // Modified after serialization
        System.out.println("Static totalBirds changed to: " + BirdData.totalBirds);
        System.out.println();
        
        // ---- DESERIALIZING ----
        System.out.println("=== After Deserialization ===");
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filename))) {
            
            BirdData restored = (BirdData) ois.readObject();
            restored.display("Restored Object");
            
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        // ---- EXPLANATION ----
        System.out.println("=== Explanation ===");
        System.out.println("Transient fields are NOT serialized:");
        System.out.println("  - Password: null (not serialized)");
        System.out.println("  - Temp Weight: 0.0 (not serialized)");
        System.out.println("Static fields are NOT serialized:");
        System.out.println("  - Total Birds: 100 (from class, not object)");
        System.out.println("Static final fields are NOT serialized:");
        System.out.println("  - Category: 'Aves' (constant)");
    }
}

transient fields are skipped during serialization. static fields are not serialized (class-level). Use transient for sensitive data. Static fields hold class-level state.

Chapter 10: Multithreading & Concurrency

10.1 Thread Creation

Thread Class

The Thread class provides built-in threading capabilities.

Extend Thread and override run(). Instantiate and call start(). Simple but limited (single inheritance). Good for basic threading.

Code Example

class BirdThread extends Thread {
    private String name;
    private int count;
    
    public BirdThread(String name, int count) {
        this.name = name;
        this.count = count;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < count; i++) {
            System.out.println(name + " is counting " + (i + 1));
            try {
                Thread.sleep(500);  // Sleep 500ms
            } catch (InterruptedException e) {
                System.out.println(name + " interrupted");
                return;
            }
        }
        System.out.println(name + " finished counting");
    }
}

public class ThreadClassExample {
    public static void main(String[] args) {
        System.out.println("Main thread started");
        
        // Creating threads
        BirdThread sparrow = new BirdThread("Sparrow", 5);
        BirdThread eagle = new BirdThread("Eagle", 3);
        
        // Starting threads
        sparrow.start();
        eagle.start();
        
        // Waiting for threads to finish
        try {
            sparrow.join();
            eagle.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }
        
        System.out.println("Main thread finished");
    }
}

Extend Thread and override run(). start() creates a new thread and calls run(). join() waits for thread completion. Threads run concurrently.

Runnable Interface

The Runnable interface provides a functional approach to threading.

Implement Runnable and pass to Thread. More flexible than extending Thread. Can extend another class. Supports lambda expressions.

Code Example

import java.util.*;

// ---- IMPLEMENTING RUNNABLE ----
class BirdRunnable implements Runnable {
    private String name;
    private int count;
    
    public BirdRunnable(String name, int count) {
        this.name = name;
        this.count = count;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < count; i++) {
            System.out.println(name + " counted bird " + (i + 1));
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                System.out.println(name + " interrupted");
                return;
            }
        }
        System.out.println(name + " finished");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        System.out.println("=== Runnable Example ===");
        System.out.println("Main thread started");
        
        // ---- TRADITIONAL RUNNABLE ----
        BirdRunnable sparrowTask = new BirdRunnable("Sparrow", 4);
        BirdRunnable eagleTask = new BirdRunnable("Eagle", 3);
        
        Thread sparrowThread = new Thread(sparrowTask);
        Thread eagleThread = new Thread(eagleTask);
        
        sparrowThread.start();
        eagleThread.start();
        
        // ---- LAMBDA RUNNABLE ----
        System.out.println("\n=== Lambda Runnable ===");
        Runnable hawkTask = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Hawk counted " + (i + 1));
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    System.out.println("Hawk interrupted");
                }
            }
        };
        
        Thread hawkThread = new Thread(hawkTask);
        hawkThread.start();
        
        // ---- RUNNABLE WITH LAMBDA DIRECTLY ----
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Cardinal: " + (i + 1));
            }
        }).start();
        
        // ---- WAITING FOR THREADS ----
        try {
            sparrowThread.join();
            eagleThread.join();
            hawkThread.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }
        
        System.out.println("Main thread finished");
    }
}

Implement Runnable and override run(). Pass to Thread constructor. More flexible than extending Thread. Lambda expressions provide concise syntax.

Callable Interface

Callable is similar to Runnable but returns a result and can throw exceptions.

Implement Callable<V> with call() returning V. Use with ExecutorService and Future. Supports return values. Can throw checked exceptions.

Code Example

import java.util.*;
import java.util.concurrent.*;

// ---- IMPLEMENTING CALLABLE ----
class BirdCounter implements Callable<Integer> {
    private String species;
    private int count;
    
    public BirdCounter(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    @Override
    public Integer call() throws Exception {
        System.out.println("Counting " + species + " birds...");
        int total = 0;
        for (int i = 0; i < count; i++) {
            total++;
            System.out.println(species + " count: " + total);
            Thread.sleep(300);
        }
        return total;
    }
}

// ---- CALLABLE WITH EXCEPTION ----
class BirdProcessor implements Callable<String> {
    private String data;
    
    public BirdProcessor(String data) {
        this.data = data;
    }
    
    @Override
    public String call() throws Exception {
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("Invalid bird data");
        }
        return "Processed: " + data.toUpperCase();
    }
}

public class CallableExample {
    public static void main(String[] args) {
        System.out.println("=== Callable Example ===");
        
        // ---- USING EXECUTOR SERVICE ----
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        try {
            // Submit Callable tasks
            Future<Integer> sparrowFuture = executor.submit(new BirdCounter("Sparrow", 3));
            Future<Integer> eagleFuture = executor.submit(new BirdCounter("Eagle", 2));
            Future<String> processorFuture = executor.submit(new BirdProcessor("Eagle"));
            
            // Get results (blocks until complete)
            int sparrowCount = sparrowFuture.get();
            int eagleCount = eagleFuture.get();
            String processed = processorFuture.get();
            
            System.out.println("\nResults:");
            System.out.println("Sparrow count: " + sparrowCount);
            System.out.println("Eagle count: " + eagleCount);
            System.out.println("Processed data: " + processed);
            
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error: " + e.getMessage());
        } finally {
            executor.shutdown();
        }
        
        // ---- CALLABLE WITH TIMEOUT ----
        System.out.println("\n=== Callable with Timeout ===");
        ExecutorService timeoutExecutor = Executors.newSingleThreadExecutor();
        
        Future<Integer> timeoutFuture = timeoutExecutor.submit(() -> {
            Thread.sleep(5000);
            return 42;
        });
        
        try {
            // Wait for result with timeout
            Integer result = timeoutFuture.get(2, TimeUnit.SECONDS);
            System.out.println("Result: " + result);
        } catch (TimeoutException e) {
            System.out.println("Task timed out!");
            timeoutFuture.cancel(true);
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error: " + e.getMessage());
        } finally {
            timeoutExecutor.shutdown();
        }
    }
}

Callable returns values and throws exceptions. Use with ExecutorService. Future gets the result. get() blocks until complete. Timeouts prevent indefinite waiting.

10.2 Thread Life Cycle & Methods

Thread States

Threads go through different states during their lifecycle.

New created but not started. Runnable ready to run. Blocked waiting for a lock. Waiting waiting indefinitely. Timed Waiting waiting with timeout. Terminated finished execution.

Code Example

public class ThreadStates {
    public static void main(String[] args) throws InterruptedException {
        // ---- NEW STATE ----
        System.out.println("=== New State ===");
        Thread thread = new Thread(() -> {
            System.out.println("Thread running");
        });
        System.out.println("After creation: " + thread.getState());
        
        // ---- RUNNABLE STATE ----
        System.out.println("\n=== Runnable State ===");
        thread.start();
        System.out.println("After start: " + thread.getState());
        
        // ---- TIMED WAITING ----
        System.out.println("\n=== Timed Waiting ===");
        Thread waitingThread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
        });
        waitingThread.start();
        Thread.sleep(100);  // Let thread start sleeping
        System.out.println("During sleep: " + waitingThread.getState());
        
        // ---- WAITING STATE ----
        System.out.println("\n=== Waiting State ===");
        Object lock = new Object();
        Thread waitingForNotify = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    System.out.println("Interrupted");
                }
            }
        });
        waitingForNotify.start();
        Thread.sleep(100);  // Let thread start waiting
        System.out.println("During wait: " + waitingForNotify.getState());
        
        // Wake up waiting thread
        synchronized (lock) {
            lock.notify();
        }
        
        // ---- BLOCKED STATE ----
        System.out.println("\n=== Blocked State ===");
        Object blocker = new Object();
        
        Thread thread1 = new Thread(() -> {
            synchronized (blocker) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted");
                }
            }
        });
        
        Thread thread2 = new Thread(() -> {
            synchronized (blocker) {
                System.out.println("Thread2 got lock");
            }
        });
        
        thread1.start();
        Thread.sleep(100);  // Let thread1 get lock
        thread2.start();
        Thread.sleep(100);  // Let thread2 try to get lock
        System.out.println("Thread2 state: " + thread2.getState());
        
        // ---- TERMINATED STATE ----
        System.out.println("\n=== Terminated State ===");
        Thread finished = new Thread(() -> {});
        finished.start();
        Thread.sleep(100);
        System.out.println("After completion: " + finished.getState());
    }
}

Threads transition between states. start() moves to Runnable. sleep() and wait() move to Waiting/Timed Waiting. synchronized can cause Blocked state. After completion, Thread enters Terminated state.

sleep()

sleep() pauses thread execution for a specified time.

Thread.sleep(millis) pauses the current thread. May throw InterruptedException. TimeUnit.sleep() is more readable. Use for timing and delays.

Code Example

import java.util.concurrent.TimeUnit;

public class SleepExample {
    public static void main(String[] args) {
        System.out.println("=== Sleep Example ===");
        
        // ---- BASIC SLEEP ----
        Thread birdThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Bird " + (i + 1) + " chirping");
                try {
                    Thread.sleep(500);  // Sleep 500ms
                } catch (InterruptedException e) {
                    System.out.println("Bird interrupted");
                    return;
                }
            }
        });
        
        birdThread.start();
        
        // ---- TIMEUNIT SLEEP ----
        System.out.println("\n=== TimeUnit Sleep ===");
        Thread.sleepThread = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Sleeping thread: " + (i + 1));
                try {
                    TimeUnit.SECONDS.sleep(1);  // More readable
                } catch (InterruptedException e) {
                    System.out.println("Interrupted");
                    return;
                }
            }
        });
        sleepThread.start();
        
        // ---- SLEEP IN MAIN ----
        System.out.println("\n=== Main Thread Sleep ===");
        System.out.println("Main sleeping for 2 seconds...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            System.out.println("Main interrupted");
        }
        System.out.println("Main woke up");
        
        // ---- INTERRUPTING SLEEP ----
        System.out.println("\n=== Interrupting Sleep ===");
        Thread interruptThread = new Thread(() -> {
            try {
                System.out.println("Thread going to sleep");
                Thread.sleep(10000);
                System.out.println("Thread woke up naturally");
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted during sleep");
            }
        });
        interruptThread.start();
        
        // Interrupt after 1 second
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Main interrupted");
        }
        interruptThread.interrupt();
        
        // ---- WAITING FOR THREADS ----
        try {
            birdThread.join();
            sleepThread.join();
            interruptThread.join();
        } catch (InterruptedException e) {
            System.out.println("Main interrupted");
        }
        
        System.out.println("\nAll threads finished");
    }
}

sleep() pauses execution for a specified duration. It can be interrupted. TimeUnit provides more readable sleep methods. Sleep is useful for pacing and timing.

join()

join() waits for a thread to finish execution.

join() blocks the current thread until the target thread finishes. join(millis) waits with timeout. Useful for coordinating thread execution.

Code Example

public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Join Example ===");
        
        // ---- BASIC JOIN ----
        Thread task1 = new Thread(() -> {
            try {
                System.out.println("Task 1 started");
                Thread.sleep(1000);
                System.out.println("Task 1 finished");
            } catch (InterruptedException e) {
                System.out.println("Task 1 interrupted");
            }
        });
        
        Thread task2 = new Thread(() -> {
            try {
                System.out.println("Task 2 started");
                Thread.sleep(2000);
                System.out.println("Task 2 finished");
            } catch (InterruptedException e) {
                System.out.println("Task 2 interrupted");
            }
        });
        
        // Start both threads
        task1.start();
        task2.start();
        
        // Wait for threads to finish
        System.out.println("Waiting for tasks to complete...");
        task1.join();  // Wait for task1
        task2.join();  // Wait for task2
        System.out.println("All tasks completed");
        
        // ---- JOIN WITH TIMEOUT ----
        System.out.println("\n=== Join with Timeout ===");
        Thread longTask = new Thread(() -> {
            try {
                System.out.println("Long task started");
                Thread.sleep(5000);
                System.out.println("Long task finished");
            } catch (InterruptedException e) {
                System.out.println("Long task interrupted");
            }
        });
        
        longTask.start();
        
        // Wait for 2 seconds only
        System.out.println("Waiting for long task (2s timeout)...");
        longTask.join(2000);
        if (longTask.isAlive()) {
            System.out.println("Long task still running after timeout");
            // Continue without waiting
        } else {
            System.out.println("Long task completed");
        }
        
        // ---- JOIN WITH MULTIPLE THREADS ----
        System.out.println("\n=== Multiple Threads ===");
        Thread[] birds = new Thread[3];
        
        for (int i = 0; i < birds.length; i++) {
            final int index = i;
            birds[i] = new Thread(() -> {
                String name = "Bird " + (index + 1);
                System.out.println(name + " started");
                try {
                    Thread.sleep((long) (1000 * (index + 1)));
                } catch (InterruptedException e) {
                    System.out.println(name + " interrupted");
                }
                System.out.println(name + " finished");
            });
            birds[i].start();
        }
        
        // Wait for all birds
        for (Thread bird : birds) {
            bird.join();
        }
        System.out.println("All birds finished");
    }
}

join() makes the caller wait for the target thread to finish. Without join, threads run concurrently. Timeout prevents indefinite waiting. Join coordinates thread execution order.

interrupt()

interrupt() signals a thread to stop waiting or stop execution.

interrupt() sets the interrupt flag. The thread should check interrupted() or catch InterruptedException. isInterrupted() checks the flag. Used for graceful thread termination.

Code Example

public class InterruptExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Interrupt Example ===");
        
        // ---- INTERRUPTING SLEEP ----
        System.out.println("=== Interrupting Sleep ===");
        Thread sleepingThread = new Thread(() -> {
            try {
                System.out.println("Thread sleeping");
                Thread.sleep(10000);
                System.out.println("Thread woke up naturally");
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted during sleep");
                // Clean up and exit
                System.out.println("Cleaning up...");
                return;
            }
            System.out.println("Thread finished normally");
        });
        
        sleepingThread.start();
        
        // Interrupt after 1 second
        Thread.sleep(1000);
        sleepingThread.interrupt();
        System.out.println("Thread interrupted");
        
        // ---- INTERRUPTING WITH FLAG CHECK ----
        System.out.println("\n=== Interrupting with Flag Check ===");
        Thread countingThread = new Thread(() -> {
            int count = 0;
            while (!Thread.interrupted()) {  // Check interrupt flag
                count++;
                System.out.println("Count: " + count);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    // Interrupted during sleep
                    System.out.println("Interrupted during sleep, exiting...");
                    return;
                }
            }
            System.out.println("Interrupted, exiting gracefully");
        });
        
        countingThread.start();
        
        // Let it run for 2 seconds
        Thread.sleep(2000);
        countingThread.interrupt();
        System.out.println("Counting thread interrupted");
        
        // ---- INTERRUPTED VS ISINTERRUPTED ----
        System.out.println("\n=== interrupted() vs isInterrupted() ===");
        Thread flagThread = new Thread(() -> {
            System.out.println("Thread started");
            while (!Thread.currentThread().isInterrupted()) {
                // Do work
            }
            System.out.println("Thread stopped via isInterrupted()");
        });
        
        flagThread.start();
        Thread.sleep(100);
        flagThread.interrupt();
        
        // ---- HANDLING INTERRUPTION IN LOOPS ----
        System.out.println("\n=== Handling Interruption in Loops ===");
        Thread loopThread = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    if (Thread.interrupted()) {
                        System.out.println("Interrupted, breaking loop");
                        break;
                    }
                    System.out.println("Loop iteration: " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                System.out.println("Interrupted in sleep");
            }
            System.out.println("Loop thread finished");
        });
        
        loopThread.start();
        Thread.sleep(500);
        loopThread.interrupt();
        
        // Wait for all threads
        sleepingThread.join();
        countingThread.join();
        flagThread.join();
        loopThread.join();
        
        System.out.println("Main finished");
    }
}

interrupt() sets the interrupt flag. Sleeping threads throw InterruptedException. Check interrupted() or isInterrupted() for graceful termination. Always handle interruption cleanly.

10.3 Synchronization & Locks

Synchronized Methods

synchronized methods ensure only one thread executes them at a time.

synchronized on instance methods locks the object instance. synchronized on static methods locks the class. Prevents race conditions.

Code Example

class BirdCounter {
    private int count = 0;
    
    // Synchronized instance method
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " incremented to: " + count);
    }
    
    public synchronized int getCount() {
        return count;
    }
}

class StaticCounter {
    private static int count = 0;
    
    // Synchronized static method
    public static synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " static increment: " + count);
    }
    
    public static synchronized int getCount() {
        return count;
    }
}

public class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        // ---- INSTANCE SYNCHRONIZATION ----
        System.out.println("=== Instance Synchronization ===");
        BirdCounter counter = new BirdCounter();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
            }
        }, "Bird1");
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
            }
        }, "Bird2");
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Final count: " + counter.getCount());
        
        // ---- STATIC SYNCHRONIZATION ----
        System.out.println("\n=== Static Synchronization ===");
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                StaticCounter.increment();
            }
        }, "Static1");
        
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                StaticCounter.increment();
            }
        }, "Static2");
        
        t3.start();
        t4.start();
        t3.join();
        t4.join();
        
        System.out.println("Final static count: " + StaticCounter.getCount());
        
        // ---- DIFFERENT LOCK OBJECTS ----
        System.out.println("\n=== Different Lock Objects ===");
        BirdCounter counter1 = new BirdCounter();
        BirdCounter counter2 = new BirdCounter();
        
        // These threads use different locks (different objects)
        Thread t5 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                counter1.increment();
            }
        }, "Counter1");
        
        Thread t6 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                counter2.increment();
            }
        }, "Counter2");
        
        t5.start();
        t6.start();
        t5.join();
        t6.join();
        
        System.out.println("Counter1: " + counter1.getCount());
        System.out.println("Counter2: " + counter2.getCount());
    }
}

synchronized methods lock the object for the entire method. Only one thread can execute synchronized methods on the same object at a time. Static synchronized methods lock the class.

Synchronized Blocks

Synchronized blocks lock a specific object for a section of code.

synchronized(object) locks the specified object. More fine-grained than synchronized methods. Use different lock objects for better concurrency.

Code Example

import java.util.*;

class BirdDatabase {
    private List<String> birds = new ArrayList<>();
    private List<String> sightings = new ArrayList<>();
    
    // Lock objects
    private final Object birdLock = new Object();
    private final Object sightingLock = new Object();
    
    public void addBird(String bird) {
        synchronized (birdLock) {
            birds.add(bird);
            System.out.println(Thread.currentThread().getName() + 
                              " added bird: " + bird);
        }
    }
    
    public void addSighting(String sighting) {
        synchronized (sightingLock) {
            sightings.add(sighting);
            System.out.println(Thread.currentThread().getName() + 
                              " added sighting: " + sighting);
        }
    }
    
    public void displayAll() {
        synchronized (birdLock) {
            System.out.println("Birds: " + birds);
        }
        synchronized (sightingLock) {
            System.out.println("Sightings: " + sightings);
        }
    }
}

class BirdCounter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment(int amount) {
        synchronized (lock) {
            count += amount;
            System.out.println(Thread.currentThread().getName() + 
                              " count: " + count);
        }
    }
    
    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        // ---- MULTIPLE LOCK OBJECTS ----
        System.out.println("=== Multiple Lock Objects ===");
        BirdDatabase database = new BirdDatabase();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                database.addBird("Sparrow" + i);
            }
        }, "BirdThread");
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                database.addSighting("Park" + i);
            }
        }, "SightingThread");
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        database.displayAll();
        
        // ---- FINE-GRAINED SYNCHRONIZATION ----
        System.out.println("\n=== Fine-Grained Synchronization ===");
        BirdCounter counter = new BirdCounter();
        
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment(1);
            }
        }, "Thread3");
        
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment(2);
            }
        }, "Thread4");
        
        t3.start();
        t4.start();
        t3.join();
        t4.join();
        
        System.out.println("Final count: " + counter.getCount());
    }
}

Synchronized blocks lock specific objects. Allows finer control than synchronized methods. Use different locks for different resources. Improves concurrency by reducing contention.

ReentrantLock

ReentrantLock provides more flexible locking than synchronized.

ReentrantLock allows explicit locking and unlocking. Supports tryLock() for non-blocking attempts. lockInterruptibly() for interruptible locking. More control but requires manual lock management.

Code Example

import java.util.concurrent.locks.ReentrantLock;

class BirdLock {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();  // Explicitly lock
        try {
            count++;
            System.out.println(Thread.currentThread().getName() + 
                              " count: " + count);
        } finally {
            lock.unlock();  // Always unlock in finally
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
    
    // Try lock without blocking
    public boolean tryIncrement(int amount) {
        if (lock.tryLock()) {
            try {
                count += amount;
                System.out.println(Thread.currentThread().getName() + 
                                  " acquired lock, count: " + count);
                return true;
            } finally {
                lock.unlock();
            }
        }
        System.out.println(Thread.currentThread().getName() + " couldn't acquire lock");
        return false;
    }
}

public class ReentrantLockExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== ReentrantLock Example ===");
        BirdLock counter = new BirdLock();
        
        // ---- BASIC LOCK ----
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
            }
        }, "Thread1");
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
            }
        }, "Thread2");
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + counter.getCount());
        
        // ---- TRY LOCK ----
        System.out.println("\n=== TryLock Example ===");
        BirdLock lockCounter = new BirdLock();
        
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                lockCounter.tryIncrement(1);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
            }
        }, "Thread3");
        
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                lockCounter.tryIncrement(1);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
            }
        }, "Thread4");
        
        t3.start();
        t4.start();
        t3.join();
        t4.join();
        System.out.println("Final try-lock count: " + lockCounter.getCount());
        
        // ---- LOCK FAIRNESS ----
        System.out.println("\n=== Fair Lock vs Unfair Lock ===");
        ReentrantLock fairLock = new ReentrantLock(true);
        ReentrantLock unfairLock = new ReentrantLock(false);
        
        System.out.println("Fair lock: threads acquire in FIFO order");
        System.out.println("Unfair lock: threads can barge");
        System.out.println("Fair locks have slightly lower performance");
    }
}

ReentrantLock provides explicit locking. lock() blocks until lock is acquired. tryLock() attempts non-blocking. Always unlock in finally block. Fairness option available.

10.4 Thread Safety & Race Conditions

Race Conditions

Race conditions occur when threads access shared data without synchronization.

Race conditions cause unpredictable results. Multiple threads modify shared data simultaneously. Data corruption occurs. Synchronization prevents race conditions.

Code Example

class UnsafeCounter {
    private int count = 0;
    
    public void increment() {
        count++;  // NOT thread-safe
    }
    
    public int getCount() {
        return count;
    }
}

class SafeCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // Thread-safe
    }
    
    public synchronized int getCount() {
        return count;
    }
}

public class RaceConditionExample {
    public static void main(String[] args) throws InterruptedException {
        // ---- RACE CONDITION DEMONSTRATION ----
        System.out.println("=== Race Condition ===");
        UnsafeCounter unsafe = new UnsafeCounter();
        
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    unsafe.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("Unsafe expected: 100000");
        System.out.println("Unsafe actual: " + unsafe.getCount());  // Inconsistent!
        
        // ---- SYNCHRONIZED SOLUTION ----
        System.out.println("\n=== Synchronized Solution ===");
        SafeCounter safe = new SafeCounter();
        
        Thread[] safeThreads = new Thread[1000];
        for (int i = 0; i < safeThreads.length; i++) {
            safeThreads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    safe.increment();
                }
            });
            safeThreads[i].start();
        }
        
        for (Thread t : safeThreads) {
            t.join();
        }
        
        System.out.println("Safe expected: 100000");
        System.out.println("Safe actual: " + safe.getCount());  // Consistent!
        
        // ---- DEMONSTRATE RACE CONDITION ----
        System.out.println("\n=== Race Condition Explained ===");
        System.out.println("Count++ is NOT an atomic operation:");
        System.out.println("1. Read current value");
        System.out.println("2. Add 1");
        System.out.println("3. Write new value");
        System.out.println("Two threads can interleave these operations");
        System.out.println("Result: lost updates!");
    }
}

Race conditions occur without synchronization. Multiple threads interleave operations. Result is unpredictable. Synchronization prevents interleaving.

volatile

volatile ensures visibility of changes across threads.

volatile prevents caching of variables. Changes are immediately visible to all threads. Does NOT provide atomicity. Use for flags and simple state.

Code Example

class BirdStatus {
    private volatile boolean running = true;
    private int count = 0;
    
    public void run() {
        System.out.println("Bird thread started");
        while (running) {
            count++;
            // Do work
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                System.out.println("Bird interrupted");
            }
        }
        System.out.println("Bird stopped. Count: " + count);
    }
    
    public void stop() {
        running = false;  // Volatile ensures visibility
        System.out.println("Stop signal sent");
    }
}

class VolatileExample {
    private static volatile boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        // ---- VOLATILE FLAG ----
        System.out.println("=== Volatile Flag ===");
        BirdStatus status = new BirdStatus();
        
        Thread birdThread = new Thread(status::run);
        birdThread.start();
        
        // Let it run for 1 second
        Thread.sleep(1000);
        status.stop();
        
        birdThread.join();
        
        // ---- WITHOUT VOLATILE (DEMONSTRATION) ----
        System.out.println("\n=== Without Volatile (may not see updates) ===");
        // Without volatile, the thread might never see the flag change
        
        // ---- VOLATILE USE CASES ----
        System.out.println("\n=== When to Use Volatile ===");
        System.out.println("1. Status flags (running, stopped)");
        System.out.println("2. Configuration updates");
        System.out.println("3. Double-checked locking");
        System.out.println("4. Simple shared state (single writer)");
        System.out.println("NOT for complex operations that need atomicity");
    }
}

volatile ensures visibility across threads. Updates to the variable are made visible to main memory without relying on a thread’s local cached copy.Prevents thread-local caching. Use for flags and simple state. Does NOT provide atomicity.

Atomic Variables

Atomic variables provide thread-safe operations on single variables.

AtomicInteger, AtomicBoolean, etc., provide atomic operations. incrementAndGet(), compareAndSet(), etc. Use CPU-level CAS operations. More efficient than synchronization.

Code Example

import java.util.concurrent.atomic.*;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Atomic operation
    }
    
    public int getCount() {
        return count.get();
    }
    
    public int addAndGet(int amount) {
        return count.addAndGet(amount);
    }
    
    public boolean compareAndSet(int expected, int newValue) {
        return count.compareAndSet(expected, newValue);
    }
}

class BirdStatus {
    private AtomicBoolean running = new AtomicBoolean(true);
    private AtomicInteger count = new AtomicInteger(0);
    private AtomicReference<String> status = new AtomicReference<>("IDLE");
    
    public void run() {
        System.out.println("Bird started: " + status.get());
        while (running.get()) {
            count.incrementAndGet();
            status.set("RUNNING");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                System.out.println("Bird interrupted");
            }
        }
        status.set("STOPPED");
        System.out.println("Bird stopped. Count: " + count.get());
    }
    
    public void stop() {
        running.set(false);
        System.out.println("Stop signal sent");
    }
    
    public String getStatus() {
        return status.get();
    }
}

public class AtomicExample {
    public static void main(String[] args) throws InterruptedException {
        // ---- ATOMIC INTEGER ----
        System.out.println("=== AtomicInteger ===");
        AtomicCounter counter = new AtomicCounter();
        
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("Atomic count: " + counter.getCount());
        
        // ---- ATOMIC OPERATIONS ----
        System.out.println("\n=== Atomic Operations ===");
        AtomicInteger atomicInt = new AtomicInteger(10);
        
        System.out.println("Initial: " + atomicInt.get());
        System.out.println("addAndGet(5): " + atomicInt.addAndGet(5));
        System.out.println("incrementAndGet: " + atomicInt.incrementAndGet());
        System.out.println("decrementAndGet: " + atomicInt.decrementAndGet());
        System.out.println("compareAndSet(15, 20): " + atomicInt.compareAndSet(15, 20));
        System.out.println("After compare: " + atomicInt.get());
        
        // ---- ATOMIC REFERENCE ----
        System.out.println("\n=== AtomicReference ===");
        AtomicReference<String> ref = new AtomicReference<>("Sparrow");
        System.out.println("Initial: " + ref.get());
        ref.compareAndSet("Sparrow", "Eagle");
        System.out.println("After CAS: " + ref.get());
        
        // ---- BIRD STATUS EXAMPLE ----
        System.out.println("\n=== Bird Status with Atomic ===");
        BirdStatus status = new BirdStatus();
        
        Thread birdThread = new Thread(status::run);
        birdThread.start();
        
        Thread.sleep(1000);
        status.stop();
        
        birdThread.join();
        System.out.println("Final status: " + status.getStatus());
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("synchronized: Heavy, blocks threads");
        System.out.println("volatile: Light, for visibility only");
        System.out.println("Atomic: Light, for atomic single-variable operations");
    }
}

Atomic variables use CAS operations. They are thread-safe without locks. More efficient than synchronized for single variables. Provide atomic operations like increment, compareAndSet.

10.5 Executor Framework & Thread Pools

ExecutorService

ExecutorService manages thread pools and task execution.

ExecutorService provides a high-level API for thread management. It handles thread creation and reuse. Improves performance by reducing thread creation overhead.

Code Example

import java.util.concurrent.*;
import java.util.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== ExecutorService ===");
        
        // ---- CREATE EXECUTOR ----
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // ---- SUBMIT RUNNABLE TASKS ----
        System.out.println("Submitting Runnable tasks:");
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " running on thread " + 
                                  Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Task " + taskId + " interrupted");
                }
                System.out.println("Task " + taskId + " completed");
            });
        }
        
        // ---- SUBMIT CALLABLE TASKS ----
        System.out.println("\nSubmitting Callable tasks:");
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            final int taskId = i;
            Future<String> future = executor.submit(() -> {
                System.out.println("Callable " + taskId + " running");
                Thread.sleep(500);
                return "Result from task " + taskId;
            });
            futures.add(future);
        }
        
        // ---- GET RESULTS ----
        for (Future<String> future : futures) {
            System.out.println("Result: " + future.get());
        }
        
        // ---- SHUTDOWN ----
        System.out.println("\nShutting down executor...");
        executor.shutdown();
        
        try {
            // Wait for all tasks to complete
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
        
        System.out.println("Executor terminated");
        
        // ---- SUBMIT VS EXECUTE ----
        System.out.println("\n=== Submit vs Execute ===");
        System.out.println("submit(): Returns Future, handles Callable and Runnable");
        System.out.println("execute(): Void, only Runnable");
    }
}

ExecutorService manages thread pools. submit() returns Future. shutdown() prevents new tasks. awaitTermination() waits for completion. Use for managing concurrent tasks efficiently.

FixedThreadPool

FixedThreadPool creates a fixed number of threads for task execution.

newFixedThreadPool(n) creates n threads. Tasks are queued when all threads are busy. Reuses threads for multiple tasks. Good for known workload.

Code Example

import java.util.concurrent.*;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        System.out.println("=== FixedThreadPool ===");
        
        // ---- CREATING FIXED THREAD POOL ----
        ExecutorService fixedPool = Executors.newFixedThreadPool(3);
        
        System.out.println("Pool with 3 threads");
        
        // Submit 10 tasks (only 3 will run concurrently)
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            fixedPool.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task " + taskId + " started on " + threadName);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("Task " + taskId + " interrupted");
                }
                System.out.println("Task " + taskId + " finished on " + threadName);
            });
        }
        
        // ---- QUEUE BEHAVIOR ----
        System.out.println("\nQueue behavior:");
        System.out.println("Tasks are queued when all threads are busy");
        System.out.println("They run when a thread becomes available");
        
        // ---- SHUTDOWN ----
        fixedPool.shutdown();
        try {
            fixedPool.awaitTermination(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            fixedPool.shutdownNow();
        }
        System.out.println("All tasks completed");
        
        // ---- PERFORMANCE COMPARISON ----
        System.out.println("\n=== Performance Comparison ===");
        System.out.println("FixedThreadPool: Threads are created once and reused");
        System.out.println("This is more efficient than creating new threads for each task");
        System.out.println("Good for workloads with known resource requirements");
    }
}

FixedThreadPool maintains a fixed number of threads. Tasks are executed by available threads. Excess tasks are queued. Threads are reused. Good for controlled resource usage.

CachedThreadPool

CachedThreadPool creates threads as needed and reuses idle threads.

newCachedThreadPool() creates threads on demand. Reuses idle threads for 60 seconds. Removes unused threads. Good for many short-lived tasks.

Code Example

import java.util.concurrent.*;

public class CachedThreadPoolExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== CachedThreadPool ===");
        
        // ---- CREATING CACHED THREAD POOL ----
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        
        System.out.println("CachedThreadPool creates threads as needed");
        
        // ---- TASKS THAT CREATE THREADS ----
        System.out.println("\nSubmitting tasks (creates new threads):");
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            cachedPool.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task " + taskId + " running on " + threadName);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    System.out.println("Task interrupted");
                }
            });
        }
        
        // ---- THREAD REUSE ----
        System.out.println("\nThreads are reused for subsequent tasks");
        Thread.sleep(500);  // Let threads finish
        
        System.out.println("\nSubmitting more tasks (reuses existing threads):");
        for (int i = 10; i < 15; i++) {
            final int taskId = i;
            cachedPool.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task " + taskId + " running on " + threadName);
            });
        }
        
        // ---- SHUTDOWN ----
        cachedPool.shutdown();
        cachedPool.awaitTermination(5, TimeUnit.SECONDS);
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("FixedThreadPool: Fixed number of threads");
        System.out.println("CachedThreadPool: Creates threads on demand");
        System.out.println("CachedThreadPool: Reuses idle threads for 60 seconds");
        System.out.println("CachedThreadPool: Good for many short-lived tasks");
        System.out.println("CachedThreadPool: Can create many threads (risk of oversubscription)");
    }
}

CachedThreadPool creates threads on demand. Idle threads are kept for 60 seconds. Good for many short tasks. Can create unlimited threads (use carefully).

ScheduledThreadPool

ScheduledThreadPool schedules tasks for delayed or periodic execution.

newScheduledThreadPool(n) creates a pool for scheduled tasks. schedule() for one-time delay. scheduleAtFixedRate() for fixed-rate execution. scheduleWithFixedDelay() for fixed-delay execution.

Code Example

import java.util.concurrent.*;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== ScheduledThreadPool ===");
        
        // ---- CREATING SCHEDULED THREAD POOL ----
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
        
        // ---- SCHEDULE WITH DELAY ----
        System.out.println("Scheduling tasks with delay:");
        scheduler.schedule(() -> {
            System.out.println("Task executed after 2 seconds delay");
        }, 2, TimeUnit.SECONDS);
        
        // ---- SCHEDULE AT FIXED RATE ----
        System.out.println("\nScheduling at fixed rate (every 1 second):");
        ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {
            System.out.println("Fixed rate task: " + System.currentTimeMillis() / 1000);
        }, 0, 1, TimeUnit.SECONDS);
        
        // ---- SCHEDULE WITH FIXED DELAY ----
        System.out.println("\nScheduling with fixed delay (2 seconds after previous):");
        ScheduledFuture<?> fixedDelay = scheduler.scheduleWithFixedDelay(() -> {
            System.out.println("Fixed delay task: " + System.currentTimeMillis() / 1000);
        }, 0, 2, TimeUnit.SECONDS);
        
        // ---- SCHEDULE WITH RETURN VALUE ----
        System.out.println("\nScheduling with return value:");
        ScheduledFuture<String> result = scheduler.schedule(() -> {
            return "Scheduled result: " + System.currentTimeMillis();
        }, 1, TimeUnit.SECONDS);
        
        System.out.println("Result: " + result.get());
        
        // ---- RUN FOR A WHILE ----
        Thread.sleep(8000);
        
        // ---- CANCEL TASKS ----
        fixedRate.cancel(false);
        fixedDelay.cancel(false);
        
        // ---- SHUTDOWN ----
        scheduler.shutdown();
        scheduler.awaitTermination(5, TimeUnit.SECONDS);
        
        System.out.println("\nScheduler terminated");
        
        // ---- USE CASES ----
        System.out.println("\n=== Use Cases ===");
        System.out.println("schedule(): One-time delayed task");
        System.out.println("scheduleAtFixedRate(): Periodic task (regardless of execution time)");
        System.out.println("scheduleWithFixedDelay(): Periodic task (with delay between executions)");
    }
}

ScheduledThreadPool handles delayed and periodic tasks. scheduleAtFixedRate() runs at fixed intervals. scheduleWithFixedDelay() waits between completions. Return values from scheduled tasks.

Future and Callable

Future represents the result of an asynchronous computation. Callable produces the result.

Future.get() blocks until result is available. isDone() checks completion. cancel() attempts to cancel. Callable returns results and throws exceptions.

Code Example

import java.util.concurrent.*;
import java.util.*;

public class FutureExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== Future and Callable ===");
        
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // ---- BASIC FUTURE ----
        System.out.println("=== Basic Future ===");
        Future<Integer> future = executor.submit(() -> {
            System.out.println("Counting birds...");
            Thread.sleep(2000);
            return 42;
        });
        
        System.out.println("Task submitted");
        System.out.println("Is done: " + future.isDone());
        
        // Get result (blocks)
        Integer result = future.get();
        System.out.println("Result: " + result);
        System.out.println("Is done: " + future.isDone());
        
        // ---- FUTURE WITH TIMEOUT ----
        System.out.println("\n=== Future with Timeout ===");
        Future<Integer> timeoutFuture = executor.submit(() -> {
            Thread.sleep(3000);
            return 100;
        });
        
        try {
            Integer timeoutResult = timeoutFuture.get(1, TimeUnit.SECONDS);
            System.out.println("Result: " + timeoutResult);
        } catch (TimeoutException e) {
            System.out.println("Task timed out!");
            timeoutFuture.cancel(true);
        }
        
        // ---- LIST OF FUTURES ----
        System.out.println("\n=== List of Futures ===");
        List<Future<String>> futures = new ArrayList<>();
        
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            futures.add(executor.submit(() -> {
                Thread.sleep(500);
                return "Task " + taskId + " result";
            }));
        }
        
        System.out.println("Waiting for all tasks...");
        for (Future<String> f : futures) {
            System.out.println("Result: " + f.get());
        }
        
        // ---- FUTURE WITH EXCEPTION ----
        System.out.println("\n=== Future with Exception ===");
        Future<Integer> exceptionFuture = executor.submit(() -> {
            throw new RuntimeException("Bird error!");
        });
        
        try {
            exceptionFuture.get();
        } catch (ExecutionException e) {
            System.out.println("Exception caught: " + e.getCause().getMessage());
        }
        
        // ---- CANCEL TASK ----
        System.out.println("\n=== Cancel Task ===");
        Future<String> cancelFuture = executor.submit(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("Task interrupted during sleep");
                return "Cancelled";
            }
            return "Completed";
        });
        
        Thread.sleep(100);
        boolean cancelled = cancelFuture.cancel(true);
        System.out.println("Cancelled: " + cancelled);
        System.out.println("Is cancelled: " + cancelFuture.isCancelled());
        
        // ---- SHUTDOWN ----
        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("Future: Represents async result");
        System.out.println("Callable: Task that returns a result");
        System.out.println("Runnable: Task that does not return a result");
    }
}

Future represents pending computation. get() blocks for result. isDone() checks completion. cancel() interrupts task. Use for asynchronous operations.

Chapter 11: Networking & JDBC

11.1 Networking Basics

TCP vs UDP

TCP and UDP are transport protocols with different characteristics.

TCP is connection-oriented, reliable, ordered, and slower. UDP is connectionless, unreliable, unordered, and faster. Choose TCP for reliability, UDP for speed.

Code Example

public class TCPvsUDP {
    public static void main(String[] args) {
        System.out.println("=== TCP vs UDP ===");
        
        System.out.println("\nTCP (Transmission Control Protocol):");
        System.out.println("  - Connection-oriented");
        System.out.println("  - Reliable (guarantees delivery)");
        System.out.println("  - Ordered delivery");
        System.out.println("  - Error checking and recovery");
        System.out.println("  - Slower (more overhead)");
        System.out.println("  - Used for: HTTP, HTTPS, FTP, SMTP, SSH");
        
        System.out.println("\nUDP (User Datagram Protocol):");
        System.out.println("  - Connectionless");
        System.out.println("  - Unreliable (no guaranteed delivery)");
        System.out.println("  - Unordered delivery");
        System.out.println("  - No error recovery");
        System.out.println("  - Faster (less overhead)");
        System.out.println("  - Used for: DNS, DHCP, VoIP, streaming");
        
        System.out.println("\nChoosing Between TCP and UDP:");
        System.out.println("  - Need reliability? Use TCP");
        System.out.println("  - Need speed? Use UDP");
        System.out.println("  - Need ordered delivery? Use TCP");
        System.out.println("  - Need broadcast/multicast? Use UDP");
        System.out.println("  - Need real-time? Consider UDP");
    }
}

TCP ensures reliable ordered delivery. UDP provides fast unordered delivery. Choose based on requirements: reliability vs speed.

InetAddress

InetAddress represents an IP address.

InetAddress provides methods for working with IP addresses. getByName() resolves hostnames. getLocalHost() gets local address. isReachable() checks connectivity.

Code Example

import java.net.*;

public class InetAddressExample {
    public static void main(String[] args) {
        System.out.println("=== InetAddress Example ===");
        
        try {
            // ---- GET LOCAL HOST ----
            System.out.println("\nLocal Host:");
            InetAddress localHost = InetAddress.getLocalHost();
            System.out.println("Host: " + localHost.getHostName());
            System.out.println("Address: " + localHost.getHostAddress());
            System.out.println("Canonical: " + localHost.getCanonicalHostName());
            
            // ---- GET BY NAME ----
            System.out.println("\nBy Name:");
            InetAddress google = InetAddress.getByName("www.google.com");
            System.out.println("Google: " + google.getHostAddress());
            
            // ---- GET ALL ADDRESSES ----
            System.out.println("\nAll Addresses:");
            InetAddress[] allGoogle = InetAddress.getAllByName("www.google.com");
            for (InetAddress addr : allGoogle) {
                System.out.println("  " + addr.getHostAddress());
            }
            
            // ---- IS REACHABLE ----
            System.out.println("\nReachability:");
            System.out.println("Local host reachable: " + localHost.isReachable(5000));
            System.out.println("Google reachable: " + google.isReachable(5000));
            
            // ---- ADDRESS TYPES ----
            System.out.println("\nAddress Types:");
            InetAddress loopback = InetAddress.getLoopbackAddress();
            System.out.println("Loopback: " + loopback.getHostAddress());
            
            InetAddress any = InetAddress.getByName("0.0.0.0");
            System.out.println("Any: " + any.getHostAddress());
            
        } catch (UnknownHostException e) {
            System.out.println("Unknown host: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

InetAddress resolves hostnames to IP addresses. Provides address information. Useful for network programming.

11.2 Sockets: Server-Client Communication

ServerSocket

ServerSocket listens for incoming connections on a port.

ServerSocket binds to a port and accepts connections. accept() blocks until a client connects. Returns a Socket for communication.

Code Example

import java.io.*;
import java.net.*;

// ---- SIMPLE SERVER ----
class SimpleServer {
    public static void main(String[] args) {
        System.out.println("=== Simple Server ===");
        
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("Server listening on port 8080");
            
            // Accept client connection
            try (Socket clientSocket = serverSocket.accept()) {
                System.out.println("Client connected: " + clientSocket.getInetAddress());
                
                // Read from client
                BufferedReader reader = new BufferedReader(
                    new InputStreamReader(clientSocket.getInputStream()));
                String message = reader.readLine();
                System.out.println("Received: " + message);
                
                // Send response
                PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
                writer.println("Server received: " + message);
                
            } catch (IOException e) {
                System.out.println("Error with client: " + e.getMessage());
            }
            
        } catch (IOException e) {
            System.out.println("Server error: " + e.getMessage());
        }
    }
}

// ---- MULTI-THREADED SERVER ----
class MultiThreadedServer {
    public static void main(String[] args) {
        System.out.println("=== Multi-Threaded Server ===");
        
        try (ServerSocket serverSocket = new ServerSocket(8081)) {
            System.out.println("Server listening on port 8081");
            
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("New client connected");
                
                // Handle client in separate thread
                new Thread(() -> {
                    try {
                        handleClient(clientSocket);
                    } catch (IOException e) {
                        System.out.println("Error handling client: " + e.getMessage());
                    }
                }).start();
            }
            
        } catch (IOException e) {
            System.out.println("Server error: " + e.getMessage());
        }
    }
    
    private static void handleClient(Socket socket) throws IOException {
        try (socket) {
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            
            String message;
            while ((message = reader.readLine()) != null) {
                System.out.println("Received: " + message);
                writer.println("Echo: " + message);
            }
        }
    }
}

public class ServerSocketExample {
    public static void main(String[] args) {
        System.out.println("=== ServerSocket Example ===");
        System.out.println("SimpleServer runs on port 8080");
        System.out.println("MultiThreadedServer runs on port 8081");
        System.out.println("Run them separately to test");
    }
}

ServerSocket listens on a port. accept() blocks until a connection arrives. Each client gets a Socket. Multi-threaded servers handle multiple clients concurrently.

Socket (Client)

Socket connects to a server for communication.

Socket connects to a server’s IP and port. Provides input/output streams for communication. Close when done.

Code Example

import java.io.*;
import java.net.*;
import java.util.*;

// ---- SIMPLE CLIENT ----
class SimpleClient {
    public static void main(String[] args) {
        System.out.println("=== Simple Client ===");
        
        try (Socket socket = new Socket("localhost", 8080)) {
            System.out.println("Connected to server");
            
            // Send message
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            writer.println("Hello from client!");
            
            // Receive response
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            String response = reader.readLine();
            System.out.println("Server response: " + response);
            
        } catch (IOException e) {
            System.out.println("Client error: " + e.getMessage());
        }
    }
}

// ---- INTERACTIVE CLIENT ----
class InteractiveClient {
    public static void main(String[] args) {
        System.out.println("=== Interactive Client ===");
        
        try (Socket socket = new Socket("localhost", 8081);
             Scanner scanner = new Scanner(System.in)) {
            
            System.out.println("Connected to server");
            
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            
            while (true) {
                System.out.print("Enter message (or 'quit'): ");
                String message = scanner.nextLine();
                
                if ("quit".equalsIgnoreCase(message)) {
                    break;
                }
                
                writer.println(message);
                String response = reader.readLine();
                System.out.println("Server: " + response);
            }
            
            System.out.println("Disconnected");
            
        } catch (IOException e) {
            System.out.println("Client error: " + e.getMessage());
        }
    }
}

public class SocketClientExample {
    public static void main(String[] args) {
        System.out.println("=== Socket Client Example ===");
        System.out.println("SimpleClient connects to port 8080");
        System.out.println("InteractiveClient connects to port 8081");
        System.out.println("Run servers first");
    }
}

Socket connects to server. Provides streams for communication. Send messages with PrintWriter. Receive with BufferedReader. Close resources properly.

11.3 JDBC: Connecting to Database

DriverManager and Connection

JDBC provides database connectivity through drivers and connections.

DriverManager loads database drivers. Connection represents a database session. Use getConnection() to connect. Handle SQLException.

Code Example

import java.sql.*;

public class JDBCConnectionExample {
    public static void main(String[] args) {
        System.out.println("=== JDBC Connection ===");
        
        // Database connection parameters
        String url = "jdbc:sqlite:birds.db";
        String username = "";
        String password = "";
        
        // ---- LOAD DRIVER (Optional for newer versions) ----
        try {
            Class.forName("org.sqlite.JDBC");
            System.out.println("Driver loaded");
        } catch (ClassNotFoundException e) {
            System.out.println("Driver not found: " + e.getMessage());
        }
        
        // ---- CONNECT TO DATABASE ----
        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            System.out.println("Connected to database");
            System.out.println("Database: " + conn.getMetaData().getDatabaseProductName());
            System.out.println("Version: " + conn.getMetaData().getDatabaseProductVersion());
            
            // Create table
            createTable(conn);
            
            // Insert data
            insertBird(conn, "Sparrow", 10);
            insertBird(conn, "Eagle", 3);
            insertBird(conn, "Hawk", 5);
            
            // Query data
            queryBirds(conn);
            
        } catch (SQLException e) {
            System.out.println("Database error: " + e.getMessage());
        }
    }
    
    private static void createTable(Connection conn) throws SQLException {
        String sql = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(sql);
            System.out.println("Table created/verified");
        }
    }
    
    private static void insertBird(Connection conn, String species, int count) 
            throws SQLException {
        String sql = "INSERT INTO birds (species, count) VALUES (?, ?)";
        
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, species);
            pstmt.setInt(2, count);
            int rows = pstmt.executeUpdate();
            System.out.println("Inserted: " + species + " (rows: " + rows + ")");
        }
    }
    
    private static void queryBirds(Connection conn) throws SQLException {
        String sql = "SELECT * FROM birds";
        
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            System.out.println("\nBirds in database:");
            while (rs.next()) {
                int id = rs.getInt("id");
                String species = rs.getString("species");
                int count = rs.getInt("count");
                System.out.println("  " + id + ". " + species + ": " + count);
            }
        }
    }
}

JDBC connects to databases. DriverManager loads drivers. Connection provides session. Use SQL statements for operations. Always close resources.

Statement, PreparedStatement, CallableStatement

Statement types for executing SQL queries.

Statement for simple SQL. PreparedStatement is used to execute parameterized SQL queries, helping separate SQL commands from user-supplied data and reducing the risk of SQL injection. CallableStatement for stored procedures. PreparedStatement is recommended.

Code Example

import java.sql.*;

public class StatementTypesExample {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:birds.db";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("=== Statement Types ===");
            
            // Create table
            createTable(conn);
            
            // ---- STATEMENT ----
            System.out.println("\n1. Statement:");
            try (Statement stmt = conn.createStatement()) {
                // Simple query
                ResultSet rs = stmt.executeQuery("SELECT * FROM birds");
                while (rs.next()) {
                    System.out.println("  " + rs.getString("species"));
                }
                
                // Execute update
                int rows = stmt.executeUpdate(
                    "INSERT INTO birds (species, count) VALUES ('Cardinal', 8)");
                System.out.println("Rows inserted: " + rows);
            }
            
            // ---- PREPAREDSTATEMENT ----
            System.out.println("\n2. PreparedStatement:");
            // Parameterized query
            String insertSQL = "INSERT INTO birds (species, count) VALUES (?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
                pstmt.setString(1, "Finch");
                pstmt.setInt(2, 6);
                pstmt.executeUpdate();
                System.out.println("Finch inserted");
                
                // Query with parameter
                String querySQL = "SELECT * FROM birds WHERE count > ?";
                try (PreparedStatement queryStmt = conn.prepareStatement(querySQL)) {
                    queryStmt.setInt(1, 5);
                    ResultSet rs = queryStmt.executeQuery();
                    System.out.println("Birds with count > 5:");
                    while (rs.next()) {
                        System.out.println("  " + rs.getString("species") + 
                                         ": " + rs.getInt("count"));
                    }
                }
            }
            
            // ---- BATCH PROCESSING ----
            System.out.println("\n3. Batch Processing:");
            try (PreparedStatement batchStmt = conn.prepareStatement(
                    "INSERT INTO birds (species, count) VALUES (?, ?)")) {
                
                for (int i = 1; i <= 3; i++) {
                    batchStmt.setString(1, "Batch" + i);
                    batchStmt.setInt(2, i * 5);
                    batchStmt.addBatch();
                }
                
                int[] results = batchStmt.executeBatch();
                System.out.println("Batch rows inserted: " + results.length);
            }
            
        } catch (SQLException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void createTable(Connection conn) throws SQLException {
        String sql = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(sql);
        }
    }
}

Statement executes simple SQL. PreparedStatement uses parameters, prevents SQL injection. CallableStatement calls stored procedures. PreparedStatement is recommended for most use cases.

11.4 CRUD Operations

Create (INSERT)

INSERT operations are used to add new rows or records to a database table.

Use INSERT INTO SQL statement. Use PreparedStatement for safety. Get auto-generated keys with getGeneratedKeys().

Code Example

import java.sql.*;

public class CreateOperation {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:birds.db";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("=== CREATE (INSERT) Operations ===");
            
            createTable(conn);
            
            // ---- BASIC INSERT ----
            System.out.println("\n1. Basic Insert:");
            String sql = "INSERT INTO birds (species, count) VALUES (?, ?)";
            
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, "Sparrow");
                pstmt.setInt(2, 10);
                int rows = pstmt.executeUpdate();
                System.out.println("Inserted " + rows + " row");
            }
            
            // ---- INSERT WITH AUTO-GENERATED KEY ----
            System.out.println("\n2. Insert with Auto-generated Key:");
            String insertSQL = "INSERT INTO birds (species, count) VALUES (?, ?)";
            
            try (PreparedStatement pstmt = conn.prepareStatement(insertSQL, 
                    Statement.RETURN_GENERATED_KEYS)) {
                pstmt.setString(1, "Eagle");
                pstmt.setInt(2, 3);
                pstmt.executeUpdate();
                
                ResultSet keys = pstmt.getGeneratedKeys();
                if (keys.next()) {
                    System.out.println("Inserted ID: " + keys.getInt(1));
                }
            }
            
            // ---- INSERT MULTIPLE ROWS ----
            System.out.println("\n3. Insert Multiple Rows:");
            String[][] birds = {
                {"Hawk", "5"},
                {"Cardinal", "8"},
                {"Finch", "6"}
            };
            
            for (String[] bird : birds) {
                try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                    pstmt.setString(1, bird[0]);
                    pstmt.setInt(2, Integer.parseInt(bird[1]));
                    pstmt.executeUpdate();
                }
            }
            System.out.println("Multiple rows inserted");
            
            // ---- DISPLAY RESULTS ----
            displayBirds(conn);
            
        } catch (SQLException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void createTable(Connection conn) throws SQLException {
        String sql = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(sql);
        }
    }
    
    private static void displayBirds(Connection conn) throws SQLException {
        String sql = "SELECT * FROM birds";
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            System.out.println("\nAll birds:");
            while (rs.next()) {
                System.out.printf("  ID: %d, Species: %s, Count: %d%n",
                    rs.getInt("id"),
                    rs.getString("species"),
                    rs.getInt("count"));
            }
        }
    }
}

INSERT adds records to the database. PreparedStatement prevents SQL injection. getGeneratedKeys() retrieves auto-generated IDs.

Read (SELECT)

SELECT operations query data from the database.

Use SELECT SQL statement. ResultSet contains query results. next() iterates through rows. Use PreparedStatement for parameterized queries.

Code Example

import java.sql.*;
import java.util.*;

public class ReadOperation {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:birds.db";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("=== READ (SELECT) Operations ===");
            
            prepareData(conn);
            
            // ---- BASIC SELECT ----
            System.out.println("\n1. Basic Select:");
            String sql = "SELECT * FROM birds";
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery(sql)) {
                
                while (rs.next()) {
                    System.out.printf("  %s: %d birds%n",
                        rs.getString("species"),
                        rs.getInt("count"));
                }
            }
            
            // ---- SELECT WITH CONDITION ----
            System.out.println("\n2. Select with Condition:");
            String conditionSQL = "SELECT * FROM birds WHERE count > ?";
            try (PreparedStatement pstmt = conn.prepareStatement(conditionSQL)) {
                pstmt.setInt(1, 5);
                ResultSet rs = pstmt.executeQuery();
                
                System.out.println("Birds with more than 5:");
                while (rs.next()) {
                    System.out.printf("  %s: %d birds%n",
                        rs.getString("species"),
                        rs.getInt("count"));
                }
            }
            
            // ---- SELECT WITH ORDER ----
            System.out.println("\n3. Select with Order:");
            String orderSQL = "SELECT * FROM birds ORDER BY count DESC";
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery(orderSQL)) {
                
                System.out.println("Birds sorted by count (descending):");
                while (rs.next()) {
                    System.out.printf("  %s: %d birds%n",
                        rs.getString("species"),
                        rs.getInt("count"));
                }
            }
            
            // ---- SELECT WITH LIKE ----
            System.out.println("\n4. Select with LIKE:");
            String likeSQL = "SELECT * FROM birds WHERE species LIKE ?";
            try (PreparedStatement pstmt = conn.prepareStatement(likeSQL)) {
                pstmt.setString(1, "%ar%");
                ResultSet rs = pstmt.executeQuery();
                
                System.out.println("Birds containing 'ar':");
                while (rs.next()) {
                    System.out.printf("  %s: %d birds%n",
                        rs.getString("species"),
                        rs.getInt("count"));
                }
            }
            
            // ---- SELECT WITH AGGREGATION ----
            System.out.println("\n5. Select with Aggregation:");
            String aggSQL = "SELECT COUNT(*) as total, AVG(count) as avg FROM birds";
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery(aggSQL)) {
                
                if (rs.next()) {
                    System.out.println("Total birds: " + rs.getInt("total"));
                    System.out.println("Average count: " + rs.getDouble("avg"));
                }
            }
            
        } catch (SQLException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void prepareData(Connection conn) throws SQLException {
        // Create table if not exists
        String createSQL = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(createSQL);
        }
        
        // Clear and insert sample data
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("DELETE FROM birds");
        }
        
        String insertSQL = "INSERT INTO birds (species, count) VALUES (?, ?)";
        String[][] birds = {
            {"Sparrow", "10"},
            {"Eagle", "3"},
            {"Hawk", "5"},
            {"Cardinal", "8"},
            {"Finch", "6"},
            {"Robin", "4"},
            {"Blue Jay", "2"}
        };
        
        for (String[] bird : birds) {
            try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
                pstmt.setString(1, bird[0]);
                pstmt.setInt(2, Integer.parseInt(bird[1]));
                pstmt.executeUpdate();
            }
        }
        System.out.println("Sample data prepared");
    }
}

SELECT queries retrieve data. ResultSet provides access to results. Use next() to iterate. Parameters allow filtering. Aggregate functions provide summary statistics.

Update (UPDATE)

UPDATE operations change the existing data stored in one or more records within a database table.

Use UPDATE SQL statement. Specify table, columns, and conditions. Use PreparedStatement for safety. Check affected rows count.

Code Example

import java.sql.*;

public class UpdateOperation {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:birds.db";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("=== UPDATE Operations ===");
            
            prepareData(conn);
            displayBirds(conn, "Initial data");
            
            // ---- BASIC UPDATE ----
            System.out.println("\n1. Basic Update:");
            String sql = "UPDATE birds SET count = ? WHERE species = ?";
            
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setInt(1, 15);
                pstmt.setString(2, "Sparrow");
                int rows = pstmt.executeUpdate();
                System.out.println("Updated " + rows + " row(s)");
            }
            
            // ---- UPDATE WITH CONDITION ----
            System.out.println("\n2. Update with Condition:");
            String conditionSQL = "UPDATE birds SET count = count + 5 WHERE count < ?";
            
            try (PreparedStatement pstmt = conn.prepareStatement(conditionSQL)) {
                pstmt.setInt(1, 4);
                int rows = pstmt.executeUpdate();
                System.out.println("Updated " + rows + " row(s) (increased counts)");
            }
            
            displayBirds(conn, "After updates");
            
            // ---- UPDATE WITH CALCULATION ----
            System.out.println("\n3. Update with Calculation:");
            String calcSQL = "UPDATE birds SET count = count * 2 WHERE species LIKE ?";
            
            try (PreparedStatement pstmt = conn.prepareStatement(calcSQL)) {
                pstmt.setString(1, "%ar%");
                int rows = pstmt.executeUpdate();
                System.out.println("Updated " + rows + " row(s) (doubled counts)");
            }
            
            displayBirds(conn, "After doubling");
            
            // ---- BULK UPDATE ----
            System.out.println("\n4. Bulk Update:");
            conn.setAutoCommit(false);  // Start transaction
            
            try {
                String bulkSQL = "UPDATE birds SET count = count - 1 WHERE count > 0";
                try (Statement stmt = conn.createStatement()) {
                    int rows = stmt.executeUpdate(bulkSQL);
                    System.out.println("Bulk updated " + rows + " row(s)");
                }
                
                conn.commit();  // Commit transaction
                
            } catch (SQLException e) {
                conn.rollback();  // Rollback on error
                System.out.println("Transaction rolled back: " + e.getMessage());
            } finally {
                conn.setAutoCommit(true);  // Reset auto-commit
            }
            
            displayBirds(conn, "After bulk update");
            
        } catch (SQLException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void prepareData(Connection conn) throws SQLException {
        String createSQL = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(createSQL);
        }
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("DELETE FROM birds");
        }
        
        String insertSQL = "INSERT INTO birds (species, count) VALUES (?, ?)";
        String[][] birds = {
            {"Sparrow", "10"},
            {"Eagle", "3"},
            {"Hawk", "5"},
            {"Cardinal", "8"},
            {"Finch", "6"},
            {"Robin", "4"},
            {"Blue Jay", "2"}
        };
        
        for (String[] bird : birds) {
            try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
                pstmt.setString(1, bird[0]);
                pstmt.setInt(2, Integer.parseInt(bird[1]));
                pstmt.executeUpdate();
            }
        }
    }
    
    private static void displayBirds(Connection conn, String title) throws SQLException {
        String sql = "SELECT * FROM birds ORDER BY id";
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            System.out.println("\n" + title + ":");
            while (rs.next()) {
                System.out.printf("  %s: %d birds%n",
                    rs.getString("species"),
                    rs.getInt("count"));
            }
        }
    }
}

UPDATE modifies existing records. Use PreparedStatement for safety. Transactions ensure consistency. executeUpdate() returns affected rows.

Delete (DELETE)

DELETE operations remove records from the database.

Use DELETE SQL statement. Specify table and conditions. Use PreparedStatement for safety. Transactions for multiple deletes.

Code Example

import java.sql.*;

public class DeleteOperation {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:birds.db";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("=== DELETE Operations ===");
            
            prepareData(conn);
            displayBirds(conn, "Initial data");
            
            // ---- BASIC DELETE ----
            System.out.println("\n1. Basic Delete:");
            String sql = "DELETE FROM birds WHERE species = ?";
            
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, "Blue Jay");
                int rows = pstmt.executeUpdate();
                System.out.println("Deleted " + rows + " row(s)");
            }
            
            displayBirds(conn, "After deleting Blue Jay");
            
            // ---- DELETE WITH CONDITION ----
            System.out.println("\n2. Delete with Condition:");
            String conditionSQL = "DELETE FROM birds WHERE count < ?";
            
            try (PreparedStatement pstmt = conn.prepareStatement(conditionSQL)) {
                pstmt.setInt(1, 5);
                int rows = pstmt.executeUpdate();
                System.out.println("Deleted " + rows + " row(s) (count < 5)");
            }
            
            displayBirds(conn, "After deleting low counts");
            
            // ---- DELETE ALL (WITH TRANSACTION) ----
            System.out.println("\n3. Delete All (with Transaction):");
            conn.setAutoCommit(false);
            
            try {
                String deleteAllSQL = "DELETE FROM birds";
                try (Statement stmt = conn.createStatement()) {
                    int rows = stmt.executeUpdate(deleteAllSQL);
                    System.out.println("Deleted " + rows + " row(s)");
                }
                
                // Add check before committing
                String checkSQL = "SELECT COUNT(*) FROM birds";
                try (Statement stmt = conn.createStatement();
                     ResultSet rs = stmt.executeQuery(checkSQL)) {
                    
                    if (rs.next() && rs.getInt(1) == 0) {
                        System.out.println("All records deleted, committing");
                        conn.commit();
                    } else {
                        System.out.println("Not empty, rolling back");
                        conn.rollback();
                    }
                }
                
            } catch (SQLException e) {
                conn.rollback();
                System.out.println("Transaction rolled back: " + e.getMessage());
            } finally {
                conn.setAutoCommit(true);
            }
            
            displayBirds(conn, "Final data");
            
        } catch (SQLException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    
    private static void prepareData(Connection conn) throws SQLException {
        String createSQL = """
            CREATE TABLE IF NOT EXISTS birds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                species TEXT NOT NULL,
                count INTEGER NOT NULL
            )
            """;
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(createSQL);
        }
        
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("DELETE FROM birds");
        }
        
        String insertSQL = "INSERT INTO birds (species, count) VALUES (?, ?)";
        String[][] birds = {
            {"Sparrow", "10"},
            {"Eagle", "3"},
            {"Hawk", "5"},
            {"Cardinal", "8"},
            {"Finch", "6"},
            {"Robin", "4"},
            {"Blue Jay", "2"}
        };
        
        for (String[] bird : birds) {
            try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
                pstmt.setString(1, bird[0]);
                pstmt.setInt(2, Integer.parseInt(bird[1]));
                pstmt.executeUpdate();
            }
        }
    }
    
    private static void displayBirds(Connection conn, String title) throws SQLException {
        String sql = "SELECT * FROM birds ORDER BY id";
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            System.out.println("\n" + title + ":");
            while (rs.next()) {
                System.out.printf("  %s: %d birds%n",
                    rs.getString("species"),
                    rs.getInt("count"));
            }
        }
    }
}

DELETE removes records. Use PreparedStatement for safety. Transactions ensure atomicity. Always verify deletion conditions.

Chapter 12: Advanced Java

12.1 Lambda Expressions & Functional Interfaces

Lambda Expressions

Lambda expressions provide a concise way to implement functional interfaces.

Lambda syntax: (parameters) -> expression or (parameters) -> { statements }. Provides anonymous function implementation. Used with functional interfaces (single abstract method).

Code Example

import java.util.*;
import java.util.function.*;

public class LambdaExample {
    public static void main(String[] args) {
        System.out.println("=== Lambda Expressions ===");
        
        // ---- BASIC LAMBDA ----
        System.out.println("\n1. Basic Lambda:");
        
        // Without lambda (anonymous class)
        Runnable oldWay = new Runnable() {
            @Override
            public void run() {
                System.out.println("Old way");
            }
        };
        
        // With lambda
        Runnable newWay = () -> System.out.println("Lambda way");
        newWay.run();
        
        // ---- LAMBDA WITH PARAMETERS ----
        System.out.println("\n2. Lambda with Parameters:");
        
        // Two parameters
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        System.out.println("Sum: " + add.apply(10, 5));
        
        // One parameter with explicit type
        Function<String, String> upper = (String s) -> s.toUpperCase();
        System.out.println("Upper: " + upper.apply("sparrow"));
        
        // One parameter with inferred type
        Function<Integer, Integer> square = x -> x * x;
        System.out.println("Square: " + square.apply(5));
        
        // ---- LAMBDA WITH BLOCK ----
        System.out.println("\n3. Lambda with Block:");
        
        Function<Integer, String> describe = x -> {
            String result = x + " birds";
            return result;
        };
        System.out.println(describe.apply(10));
        
        // ---- LAMBDA IN COLLECTIONS ----
        System.out.println("\n4. Lambda with Collections:");
        
        List<String> birds = Arrays.asList("Sparrow", "Eagle", "Hawk");
        
        // Sorting with lambda
        birds.sort((a, b) -> a.compareTo(b));
        System.out.println("Sorted: " + birds);
        
        // Iterating with lambda
        birds.forEach(bird -> System.out.println("  " + bird));
        
        // ---- LAMBDA WITH FILTER ----
        System.out.println("\n5. Lambda with Filter:");
        
        List<String> filtered = birds.stream()
            .filter(b -> b.startsWith("S"))
            .map(b -> b.toUpperCase())
            .toList();
        System.out.println("Filtered: " + filtered);
        
        // ---- CUSTOM FUNCTIONAL INTERFACE ----
        System.out.println("\n6. Custom Functional Interface:");
        
        // Using custom interface with lambda
        BirdProcessor processor = name -> System.out.println("Processing: " + name);
        processor.process("Sparrow");
        
        // With multiple statements
        BirdProcessor detailed = name -> {
            System.out.println("Received: " + name);
            System.out.println("Processing: " + name.toUpperCase());
        };
        detailed.process("Eagle");
    }
    
    @FunctionalInterface
    interface BirdProcessor {
        void process(String name);
    }
}

Lambdas implement functional interfaces. Syntax is concise. Works with streams and collections. Provide functional programming style.

Functional Interfaces

Functional interfaces have a single abstract method, enabling lambda usage.

Common functional interfaces: Predicate<T> (boolean test), Function<T,R> (apply), Consumer<T> (accept), Supplier<T> (get), BinaryOperator<T> (apply).

Code Example

import java.util.*;
import java.util.function.*;

public class FunctionalInterfacesExample {
    public static void main(String[] args) {
        System.out.println("=== Functional Interfaces ===");
        
        // ---- PREDICATE ----
        System.out.println("\n1. Predicate:");
        Predicate<String> startsWithS = s -> s.startsWith("S");
        System.out.println("Sparrow starts with S: " + startsWithS.test("Sparrow"));
        System.out.println("Eagle starts with S: " + startsWithS.test("Eagle"));
        
        Predicate<Integer> isPositive = n -> n > 0;
        System.out.println("10 is positive: " + isPositive.test(10));
        System.out.println("-5 is positive: " + isPositive.test(-5));
        
        // ---- FUNCTION ----
        System.out.println("\n2. Function:");
        Function<String, Integer> length = s -> s.length();
        System.out.println("Length of Sparrow: " + length.apply("Sparrow"));
        
        Function<Integer, Integer> doubleValue = n -> n * 2;
        System.out.println("Double of 5: " + doubleValue.apply(5));
        
        // ---- CONSUMER ----
        System.out.println("\n3. Consumer:");
        Consumer<String> print = s -> System.out.println("  " + s);
        print.accept("Sparrow");
        print.accept("Eagle");
        
        // ---- SUPPLIER ----
        System.out.println("\n4. Supplier:");
        Supplier<Double> random = () -> Math.random();
        System.out.println("Random: " + random.get());
        System.out.println("Random: " + random.get());
        
        // ---- BINARYOPERATOR ----
        System.out.println("\n5. BinaryOperator:");
        BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
        System.out.println("Max of 5 and 10: " + max.apply(5, 10));
        
        BinaryOperator<String> concat = (a, b) -> a + " " + b;
        System.out.println("Concat: " + concat.apply("Sparrow", "Eagle"));
        
        // ---- COMBINING FUNCTIONAL INTERFACES ----
        System.out.println("\n6. Combining:");
        List<String> birds = Arrays.asList("Sparrow", "Eagle", "Hawk", "Cardinal");
        
        Predicate<String> startsWithSOrE = s -> s.startsWith("S") || s.startsWith("E");
        birds.stream()
            .filter(startsWithSOrE)
            .map(String::toUpperCase)
            .forEach(System.out::println);
    }
}

Functional interfaces define single-abstract-method contracts. Predicate tests conditions. Function transforms values. Consumer accepts values. Supplier provides values.

12.2 Streams API & Parallel Streams

Intermediate Operations

Intermediate operations transform streams and are lazy.

Operations like filter, map, sorted, distinct, limit. They return new streams. Lazy evaluation means they don’t execute until terminal operation is called.

Code Example

import java.util.*;
import java.util.stream.*;

public class StreamIntermediateExample {
    public static void main(String[] args) {
        System.out.println("=== Stream Intermediate Operations ===");
        
        List<String> birds = Arrays.asList(
            "Sparrow", "Eagle", "Hawk", "Cardinal", 
            "Finch", "Robin", "Blue Jay", "Sparrow"
        );
        
        System.out.println("Original list: " + birds);
        
        // ---- FILTER ----
        System.out.println("\n1. filter:");
        birds.stream()
            .filter(b -> b.length() > 5)
            .forEach(System.out::println);
        
        // ---- MAP ----
        System.out.println("\n2. map:");
        birds.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);
        
        // ---- SORTED ----
        System.out.println("\n3. sorted:");
        birds.stream()
            .sorted()
            .forEach(System.out::println);
        
        // ---- DISTINCT ----
        System.out.println("\n4. distinct:");
        birds.stream()
            .distinct()
            .forEach(System.out::println);
        
        // ---- LIMIT ----
        System.out.println("\n5. limit:");
        birds.stream()
            .limit(3)
            .forEach(System.out::println);
        
        // ---- SKIP ----
        System.out.println("\n6. skip:");
        birds.stream()
            .skip(3)
            .forEach(System.out::println);
        
        // ---- CHAINING OPERATIONS ----
        System.out.println("\n7. Chaining:");
        birds.stream()
            .filter(b -> b.length() > 4)
            .map(String::toLowerCase)
            .distinct()
            .sorted()
            .limit(3)
            .forEach(System.out::println);
        
        // ---- PEEK (debugging) ----
        System.out.println("\n8. peek (debugging):");
        birds.stream()
            .peek(b -> System.out.println("Before: " + b))
            .filter(b -> b.length() > 5)
            .peek(b -> System.out.println("After filter: " + b))
            .map(String::toUpperCase)
            .forEach(System.out::println);
    }
}

Intermediate operations are lazy and chainable. filter selects elements. map transforms elements. sorted orders elements. distinct removes duplicates. limit restricts size.

Terminal Operations

Terminal operations trigger stream execution and produce a result.

Terminal operations like forEach, collect, reduce, count, anyMatch. They are eager and consume the stream. Only one terminal operation per stream.

Code Example

import java.util.*;
import java.util.stream.*;

public class StreamTerminalExample {
    public static void main(String[] args) {
        System.out.println("=== Stream Terminal Operations ===");
        
        List<String> birds = Arrays.asList(
            "Sparrow", "Eagle", "Hawk", "Cardinal", 
            "Finch", "Robin", "Blue Jay"
        );
        
        System.out.println("Original: " + birds);
        
        // ---- FOR EACH ----
        System.out.println("\n1. forEach:");
        birds.stream().forEach(b -> System.out.println("  " + b));
        
        // ---- COLLECT ----
        System.out.println("\n2. collect:");
        List<String> collected = birds.stream()
            .filter(b -> b.length() > 5)
            .collect(Collectors.toList());
        System.out.println("Collected: " + collected);
        
        // ---- REDUCE ----
        System.out.println("\n3. reduce:");
        int totalLength = birds.stream()
            .map(String::length)
            .reduce(0, (a, b) -> a + b);
        System.out.println("Total length: " + totalLength);
        
        String concatenated = birds.stream()
            .reduce("", (a, b) -> a + " " + b);
        System.out.println("Concatenated:" + concatenated);
        
        // ---- COUNT ----
        System.out.println("\n4. count:");
        long count = birds.stream()
            .filter(b -> b.startsWith("S"))
            .count();
        System.out.println("Birds starting with S: " + count);
        
        // ---- ANY MATCH ----
        System.out.println("\n5. anyMatch:");
        boolean hasEagle = birds.stream()
            .anyMatch(b -> b.equals("Eagle"));
        System.out.println("Has Eagle: " + hasEagle);
        
        // ---- ALL MATCH ----
        System.out.println("\n6. allMatch:");
        boolean allLong = birds.stream()
            .allMatch(b -> b.length() > 3);
        System.out.println("All longer than 3: " + allLong);
        
        // ---- NONE MATCH ----
        System.out.println("\n7. noneMatch:");
        boolean noEmpty = birds.stream()
            .noneMatch(b -> b.isEmpty());
        System.out.println("No empty strings: " + noEmpty);
        
        // ---- FIND FIRST ----
        System.out.println("\n8. findFirst:");
        Optional<String> first = birds.stream()
            .filter(b -> b.length() > 6)
            .findFirst();
        first.ifPresent(b -> System.out.println("First long name: " + b));
        
        // ---- MIN/MAX ----
        System.out.println("\n9. min/max:");
        Optional<String> shortest = birds.stream()
            .min(Comparator.comparing(String::length));
        shortest.ifPresent(b -> System.out.println("Shortest: " + b));
    }
}

Terminal operations execute the stream pipeline. forEach iterates. collect creates collections. reduce combines elements. count counts elements. anyMatch checks conditions.

Parallel Streams

Parallel streams process data concurrently for better performance.

parallelStream() creates parallel streams. Uses Fork/Join framework. Can improve performance for large datasets. Be careful with shared state.

Code Example

import java.util.*;
import java.util.stream.*;

public class ParallelStreamExample {
    public static void main(String[] args) {
        System.out.println("=== Parallel Streams ===");
        
        // ---- CREATING PARALLEL STREAMS ----
        System.out.println("\n1. Creating Parallel Streams:");
        
        List<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            numbers.add(i);
        }
        
        // Sequential stream
        long seqStart = System.currentTimeMillis();
        numbers.stream().map(n -> n * n).collect(Collectors.toList());
        long seqEnd = System.currentTimeMillis();
        
        // Parallel stream
        long parStart = System.currentTimeMillis();
        numbers.parallelStream().map(n -> n * n).collect(Collectors.toList());
        long parEnd = System.currentTimeMillis();
        
        System.out.println("Sequential: " + (seqEnd - seqStart) + "ms");
        System.out.println("Parallel: " + (parEnd - parStart) + "ms");
        
        // ---- PARALLEL WITH FILTER ----
        System.out.println("\n2. Parallel with Filter:");
        
        List<String> birds = Arrays.asList(
            "Sparrow", "Eagle", "Hawk", "Cardinal", 
            "Finch", "Robin", "Blue Jay"
        );
        
        birds.parallelStream()
            .filter(b -> b.length() > 4)
            .forEach(b -> System.out.println(
                Thread.currentThread().getName() + ": " + b
            ));
        
        // ---- PARALLEL WITH MAP ----
        System.out.println("\n3. Parallel with Map:");
        
        List<String> upperBirds = birds.parallelStream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println("Upper: " + upperBirds);
        
        // ---- PARALLEL WITH REDUCE ----
        System.out.println("\n4. Parallel with Reduce:");
        
        int totalLength = birds.parallelStream()
            .map(String::length)
            .reduce(0, Integer::sum);
        System.out.println("Total length: " + totalLength);
        
        // ---- PARALLEL WITH ORDER ----
        System.out.println("\n5. Preserving Order:");
        
        List<String> ordered = birds.parallelStream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println("Ordered: " + ordered);
        
        // ---- WHEN TO USE PARALLEL ----
        System.out.println("\n6. When to Use Parallel Streams:");
        System.out.println("✓ Large datasets");
        System.out.println("✓ CPU-intensive operations");
        System.out.println("✓ No shared state");
        System.out.println("✗ Small datasets");
        System.out.println("✗ I/O operations");
        System.out.println("✗ Stateful operations");
    }
}

Parallel streams use multiple threads. Can improve performance for CPU-bound operations. Order is not guaranteed. Use for large datasets.

12.3 Java 8+ Features

Date-Time API

The Date-Time API provides better date and time handling.

LocalDate for dates, LocalTime for time, LocalDateTime for both. ZonedDateTime for time zones. DateTimeFormatter for formatting.

Code Example

import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

public class DateTimeExample {
    public static void main(String[] args) {
        System.out.println("=== Date-Time API ===");
        
        // ---- LOCAL DATE ----
        System.out.println("\n1. LocalDate:");
        LocalDate today = LocalDate.now();
        System.out.println("Today: " + today);
        System.out.println("Year: " + today.getYear());
        System.out.println("Month: " + today.getMonth());
        System.out.println("Day: " + today.getDayOfWeek());
        
        LocalDate specific = LocalDate.of(2024, 1, 15);
        System.out.println("Specific: " + specific);
        
        // ---- LOCAL TIME ----
        System.out.println("\n2. LocalTime:");
        LocalTime now = LocalTime.now();
        System.out.println("Now: " + now);
        System.out.println("Hour: " + now.getHour());
        System.out.println("Minute: " + now.getMinute());
        
        LocalTime specificTime = LocalTime.of(14, 30);
        System.out.println("Specific: " + specificTime);
        
        // ---- LOCAL DATE TIME ----
        System.out.println("\n3. LocalDateTime:");
        LocalDateTime dateTime = LocalDateTime.now();
        System.out.println("Now: " + dateTime);
        
        // ---- DATE MANIPULATION ----
        System.out.println("\n4. Date Manipulation:");
        LocalDate future = today.plusDays(10);
        System.out.println("+10 days: " + future);
        
        LocalDate past = today.minusMonths(2);
        System.out.println("-2 months: " + past);
        
        // ---- DATE COMPARISON ----
        System.out.println("\n5. Date Comparison:");
        LocalDate date1 = LocalDate.of(2024, 1, 15);
        LocalDate date2 = LocalDate.of(2024, 1, 20);
        
        System.out.println("date1: " + date1);
        System.out.println("date2: " + date2);
        System.out.println("date1 before date2: " + date1.isBefore(date2));
        System.out.println("date1 after date2: " + date1.isAfter(date2));
        
        // ---- FORMATTING ----
        System.out.println("\n6. Formatting:");
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
        String formatted = dateTime.format(formatter);
        System.out.println("Formatted: " + formatted);
        
        LocalDate parsed = LocalDate.parse("15-01-2024", 
            DateTimeFormatter.ofPattern("dd-MM-yyyy"));
        System.out.println("Parsed: " + parsed);
        
        // ---- TIME ZONES ----
        System.out.println("\n7. Time Zones:");
        ZonedDateTime zoned = ZonedDateTime.now();
        System.out.println("Local: " + zoned);
        
        ZoneId newYork = ZoneId.of("America/New_York");
        ZonedDateTime newYorkTime = ZonedDateTime.now(newYork);
        System.out.println("New York: " + newYorkTime);
        
        // ---- PERIOD ----
        System.out.println("\n8. Period:");
        LocalDate start = LocalDate.of(2024, 1, 1);
        LocalDate end = LocalDate.of(2024, 12, 31);
        Period period = Period.between(start, end);
        System.out.println("Months between: " + period.getMonths());
        System.out.println("Days between: " + period.getDays());
        
        // ---- DURATION ----
        System.out.println("\n9. Duration:");
        LocalTime time1 = LocalTime.of(10, 0);
        LocalTime time2 = LocalTime.of(14, 30);
        Duration duration = Duration.between(time1, time2);
        System.out.println("Hours between: " + duration.toHours());
        System.out.println("Minutes between: " + duration.toMinutes());
    }
}

Date-Time API provides immutable date/time objects. LocalDate for dates, LocalTime for time, LocalDateTime for both. Supports manipulation, comparison, and formatting.

Optional

Optional helps avoid NullPointerException by representing presence/absence of a value.

Optional containers may or may not hold a value. Provides methods like isPresent(), ifPresent(), orElse(), map(), flatMap(). Encourages checking for null.

Code Example

import java.util.*;

public class OptionalExample {
    public static void main(String[] args) {
        System.out.println("=== Optional ===");
        
        // ---- CREATING OPTIONAL ----
        System.out.println("\n1. Creating Optional:");
        
        Optional<String> present = Optional.of("Sparrow");
        Optional<String> empty = Optional.empty();
        Optional<String> nullable = Optional.ofNullable(null);
        
        System.out.println("Present: " + present);
        System.out.println("Empty: " + empty);
        System.out.println("Nullable: " + nullable);
        
        // ---- CHECKING PRESENCE ----
        System.out.println("\n2. Checking Presence:");
        
        System.out.println("Present isPresent: " + present.isPresent());
        System.out.println("Empty isPresent: " + empty.isPresent());
        System.out.println("Empty isEmpty: " + empty.isEmpty());
        
        // ---- IF PRESENT ----
        System.out.println("\n3. ifPresent:");
        present.ifPresent(b -> System.out.println("Value: " + b));
        empty.ifPresent(b -> System.out.println("This won't print"));
        
        // ---- OR ELSE ----
        System.out.println("\n4. orElse:");
        System.out.println("Present orElse: " + present.orElse("Default"));
        System.out.println("Empty orElse: " + empty.orElse("Default"));
        
        // ---- OR ELSE GET ----
        System.out.println("\n5. orElseGet:");
        System.out.println("Empty orElseGet: " + 
            empty.orElseGet(() -> "Computed default"));
        
        // ---- OR ELSE THROW ----
        System.out.println("\n6. orElseThrow:");
        try {
            String value = present.orElseThrow();
            System.out.println("Value: " + value);
            
            String emptyValue = empty.orElseThrow(() -> 
                new IllegalArgumentException("No value"));
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
        }
        
        // ---- MAP ----
        System.out.println("\n7. map:");
        Optional<String> upper = present.map(String::toUpperCase);
        System.out.println("Mapped: " + upper.orElse("Empty"));
        
        Optional<Integer> length = present.map(String::length);
        System.out.println("Length: " + length.orElse(0));
        
        // ---- FLAT MAP ----
        System.out.println("\n8. flatMap:");
        Optional<String> flat = present.flatMap(b -> 
            b.equals("Sparrow") ? Optional.of("Found") : Optional.empty());
        System.out.println("FlatMap: " + flat.orElse("Not found"));
        
        // ---- FILTER ----
        System.out.println("\n9. filter:");
        Optional<String> filtered = present.filter(b -> b.startsWith("S"));
        System.out.println("Filtered (starts S): " + filtered.orElse("None"));
        
        Optional<String> notFiltered = present.filter(b -> b.startsWith("X"));
        System.out.println("Filtered (starts X): " + notFiltered.orElse("None"));
        
        // ---- PRACTICAL USE CASE ----
        System.out.println("\n10. Practical Example:");
        System.out.println("Returning empty Optional for not found:");
        System.out.println(findBird("Sparrow").orElse("Not found"));
        System.out.println(findBird("Unknown").orElse("Not found"));
    }
    
    public static Optional<String> findBird(String name) {
        List<String> birds = Arrays.asList("Sparrow", "Eagle", "Hawk");
        return birds.stream()
            .filter(b -> b.equals(name))
            .findFirst();
    }
}

Optional represents presence/absence of a value. Prevents NullPointerException. Provides methods for safe handling. Use for return values where null is possible.

Method References

Method references provide shorthand for lambda expressions.

Four types: Class::staticMethod, object::instanceMethod, Class::instanceMethod, Class::new (constructor). They’re compact and readable.

Code Example

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class MethodReferences {
    public static void main(String[] args) {
        System.out.println("=== Method References ===");
        
        // ---- STATIC METHOD REFERENCE ----
        System.out.println("\n1. Static Method Reference (Class::method):");
        List<String> birds = Arrays.asList("Sparrow", "Eagle", "Hawk");
        
        // Without method reference
        birds.forEach(b -> System.out.println(b));
        
        // With method reference
        birds.forEach(System.out::println);
        
        // Static method with custom class
        birds.stream().map(MethodReferences::toUpper).forEach(System.out::println);
        
        // ---- INSTANCE METHOD REFERENCE ----
        System.out.println("\n2. Instance Method Reference (object::method):");
        MethodReferences helper = new MethodReferences();
        birds.forEach(helper::printBird);
        
        // ---- CLASS INSTANCE METHOD REFERENCE ----
        System.out.println("\n3. Class Instance Method Reference (Class::method):");
        birds.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);
        
        birds.sort(String::compareToIgnoreCase);
        System.out.println("Sorted: " + birds);
        
        // ---- CONSTRUCTOR REFERENCE ----
        System.out.println("\n4. Constructor Reference (Class::new):");
        Supplier<Bird> birdSupplier = Bird::new;
        Bird newBird = birdSupplier.get();
        
        Function<String, Bird> birdCreator = Bird::new;
        Bird sparrow = birdCreator.apply("Sparrow");
        sparrow.display();
        
        // ---- COMPARISON ----
        System.out.println("\n5. Comparison:");
        System.out.println("Lambda: (b) -> b.length()");
        System.out.println("Reference: String::length");
        System.out.println("\nLambda: (a, b) -> a.compareTo(b)");
        System.out.println("Reference: String::compareTo");
    }
    
    public static String toUpper(String s) {
        return s.toUpperCase();
    }
    
    public void printBird(String bird) {
        System.out.println("Bird: " + bird);
    }
}

class Bird {
    private String species;
    
    public Bird() {
        this("Unknown");
    }
    
    public Bird(String species) {
        this.species = species;
    }
    
    public void display() {
        System.out.println("Bird: " + species);
    }
}

Method references are compact lambda equivalents. Static references use Class::method. Instance references use object::method. Constructor references use Class::new.

CompletableFuture

CompletableFuture provides asynchronous programming with chaining and composition.

CompletableFuture represents async computations. Supports chaining with thenApply, thenAccept, thenCompose. Combines with allOf, anyOf. Provides timeouts.

Code Example

import java.util.concurrent.*;
import java.util.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== CompletableFuture ===");
        
        // ---- BASIC COMPLETABLE FUTURE ----
        System.out.println("\n1. Basic CompletableFuture:");
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Computing on: " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
            return "Sparrow";
        });
        
        String result = future.get();
        System.out.println("Result: " + result);
        
        // ---- CHAINING ----
        System.out.println("\n2. Chaining:");
        CompletableFuture<String> chain = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
            return "eagle";
        }).thenApply(s -> s.toUpperCase())
          .thenApply(s -> s + "!");
        
        System.out.println("Chained: " + chain.get());
        
        // ---- THEN ACCEPT ----
        System.out.println("\n3. thenAccept:");
        CompletableFuture<Void> accepted = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
            return "Hawk";
        }).thenAccept(bird -> System.out.println("Bird: " + bird));
        
        accepted.get();
        
        // ---- THEN COMPOSE ----
        System.out.println("\n4. thenCompose:");
        CompletableFuture<String> composed = CompletableFuture.supplyAsync(() -> {
            return "Cardinal";
        }).thenCompose(bird -> CompletableFuture.supplyAsync(() -> {
            return bird + " (processed)";
        }));
        
        System.out.println("Composed: " + composed.get());
        
        // ---- COMBINING ----
        System.out.println("\n5. Combining Futures:");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) {}
            return "Sparrow";
        });
        
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(300); } catch (InterruptedException e) {}
            return "Eagle";
        });
        
        CompletableFuture<String> combined = future1.thenCombine(future2, 
            (b1, b2) -> b1 + " and " + b2);
        
        System.out.println("Combined: " + combined.get());
        
        // ---- ALL OF ----
        System.out.println("\n6. allOf:");
        List<CompletableFuture<String>> futures = new ArrayList<>();
        String[] birds = {"Sparrow", "Eagle", "Hawk"};
        
        for (String bird : birds) {
            futures.add(CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 100));
                } catch (InterruptedException e) {}
                return bird.toUpperCase();
            }));
        }
        
        CompletableFuture<Void> allOf = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]));
        
        allOf.join();
        
        for (CompletableFuture<String> f : futures) {
            System.out.println("Result: " + f.get());
        }
        
        // ---- TIMEOUT ----
        System.out.println("\n7. Timeout:");
        CompletableFuture<String> timeout = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {}
            return "Slow result";
        }).completeOnTimeout("Timeout", 1, TimeUnit.SECONDS);
        
        System.out.println("With timeout: " + timeout.get());
        
        // ---- EXCEPTION HANDLING ----
        System.out.println("\n8. Exception Handling:");
        CompletableFuture<String> exception = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("Bird error!");
        }).exceptionally(e -> "Error: " + e.getMessage());
        
        System.out.println("Exception result: " + exception.get());
    }
}

CompletableFuture represents async computations. Supports chaining with thenApply, thenAccept. Combines multiple futures. Handles timeouts and exceptions.

12.4 Annotations & Reflection

Annotations

Annotations add metadata to code for the compiler and runtime.

Built-in annotations: @Override, @Deprecated, @SuppressWarnings. Custom annotations with @interface. Retention policies: SOURCE, CLASS, RUNTIME. Targets: METHOD, FIELD, TYPE, etc.

Code Example

import java.lang.annotation.*;
import java.lang.reflect.*;

// ---- CUSTOM ANNOTATION ----
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface BirdInfo {
    String species() default "Unknown";
    int count() default 0;
    boolean endangered() default false;
}

// ---- ANOTHER CUSTOM ANNOTATION ----
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MaxValue {
    int value();
}

// ---- USING ANNOTATIONS ----
@BirdInfo(species = "Sparrow", count = 10)
class Sparrow {
    @MaxValue(100)
    private int count;
    
    @BirdInfo(species = "House Sparrow")
    public void display() {
        System.out.println("Sparrow display");
    }
    
    public void setCount(int count) {
        this.count = count;
    }
}

@BirdInfo(species = "Eagle", count = 3, endangered = true)
class Eagle {
    @BirdInfo(species = "Bald Eagle")
    public void display() {
        System.out.println("Eagle display");
    }
}

public class AnnotationExample {
    public static void main(String[] args) {
        System.out.println("=== Annotations ===");
        
        // ---- BUILT-IN ANNOTATIONS ----
        System.out.println("\n1. Built-in Annotations:");
        Bird bird = new Bird();
        bird.display();
        
        // ---- READING ANNOTATIONS ----
        System.out.println("\n2. Reading Annotations:");
        readAnnotations(Sparrow.class);
        readAnnotations(Eagle.class);
        
        // ---- FIELD ANNOTATIONS ----
        System.out.println("\n3. Field Annotations:");
        try {
            Field field = Sparrow.class.getDeclaredField("count");
            MaxValue max = field.getAnnotation(MaxValue.class);
            if (max != null) {
                System.out.println("Max value: " + max.value());
            }
        } catch (NoSuchFieldException e) {
            System.out.println("Field not found");
        }
    }
    
    private static void readAnnotations(Class<?> clazz) {
        System.out.println("Class: " + clazz.getSimpleName());
        
        BirdInfo info = clazz.getAnnotation(BirdInfo.class);
        if (info != null) {
            System.out.println("  Species: " + info.species());
            System.out.println("  Count: " + info.count());
            System.out.println("  Endangered: " + info.endangered());
        }
        
        // Method annotations
        for (Method method : clazz.getDeclaredMethods()) {
            BirdInfo methodInfo = method.getAnnotation(BirdInfo.class);
            if (methodInfo != null) {
                System.out.println("  Method: " + method.getName());
                System.out.println("    Species: " + methodInfo.species());
            }
        }
    }
}

class Bird {
    @Override  // Built-in annotation
    public String toString() {
        return "Bird";
    }
    
    @Deprecated  // Built-in annotation
    public void oldMethod() {
        System.out.println("Deprecated method");
    }
    
    @SuppressWarnings("unused")
    public void suppressWarning() {
        int unused = 10;  // No warning
    }
    
    public void display() {
        System.out.println("Bird display");
    }
}

Annotations provide metadata. @Override ensures method overrides. @Deprecated marks obsolete code. @SuppressWarnings suppresses compiler warnings. Custom annotations can be read at runtime with reflection.

Reflection

Reflection allows inspecting and manipulating classes at runtime.

Reflection provides access to classes, methods, fields, and constructors at runtime. Used in frameworks, debugging, and dynamic code. Can impact performance.

Code Example

import java.lang.reflect.*;
import java.util.*;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== Reflection ===");
        
        // ---- CLASS INSPECTION ----
        System.out.println("\n1. Class Inspection:");
        Class<?> birdClass = Bird.class;
        
        System.out.println("Class name: " + birdClass.getName());
        System.out.println("Simple name: " + birdClass.getSimpleName());
        System.out.println("Package: " + birdClass.getPackageName());
        
        // ---- METHODS ----
        System.out.println("\n2. Methods:");
        Method[] methods = birdClass.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("  " + method.getName() + "()");
            System.out.println("    Return: " + method.getReturnType());
            System.out.println("    Parameters: " + method.getParameterCount());
        }
        
        // ---- FIELDS ----
        System.out.println("\n3. Fields:");
        Field[] fields = birdClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("  " + field.getName() + ": " + field.getType());
        }
        
        // ---- CONSTRUCTORS ----
        System.out.println("\n4. Constructors:");
        Constructor<?>[] constructors = birdClass.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println("  Constructor: " + constructor.getParameterCount() + " params");
        }
        
        // ---- CREATING INSTANCE ----
        System.out.println("\n5. Creating Instance:");
        Constructor<?> constructor = birdClass.getDeclaredConstructor(String.class, int.class);
        Object bird = constructor.newInstance("Sparrow", 10);
        System.out.println("Created: " + bird);
        
        // ---- INVOKING METHODS ----
        System.out.println("\n6. Invoking Methods:");
        Method displayMethod = birdClass.getDeclaredMethod("display");
        displayMethod.invoke(bird);
        
        Method getSpecies = birdClass.getDeclaredMethod("getSpecies");
        String species = (String) getSpecies.invoke(bird);
        System.out.println("Species: " + species);
        
        // ---- ACCESSING FIELDS ----
        System.out.println("\n7. Accessing Fields:");
        Field speciesField = birdClass.getDeclaredField("species");
        speciesField.setAccessible(true);  // Access private field
        
        String currentSpecies = (String) speciesField.get(bird);
        System.out.println("Current species: " + currentSpecies);
        
        speciesField.set(bird, "Eagle");
        System.out.println("Changed species: " + speciesField.get(bird));
        
        // ---- PRIVATE METHOD ----
        System.out.println("\n8. Private Method:");
        Method privateMethod = birdClass.getDeclaredMethod("privateMethod");
        privateMethod.setAccessible(true);
        privateMethod.invoke(bird);
        
        // ---- ARRAY REFLECTION ----
        System.out.println("\n9. Array Reflection:");
        Object array = Array.newInstance(String.class, 3);
        Array.set(array, 0, "Sparrow");
        Array.set(array, 1, "Eagle");
        Array.set(array, 2, "Hawk");
        
        System.out.println("Array length: " + Array.getLength(array));
        System.out.println("Element 0: " + Array.get(array, 0));
        
        // ---- COMPARISON ----
        System.out.println("\n=== Comparison ===");
        System.out.println("Reflection: Runtime inspection and manipulation");
        System.out.println("Pros: Dynamic, flexible");
        System.out.println("Cons: Slower, less safe");
        System.out.println("Used in: Frameworks, serialization, testing");
    }
}

class Bird {
    private String species;
    private int count;
    
    public Bird() {
        this("Unknown", 0);
    }
    
    public Bird(String species, int count) {
        this.species = species;
        this.count = count;
    }
    
    public void display() {
        System.out.println("Species: " + species + ", Count: " + count);
    }
    
    public String getSpecies() {
        return species;
    }
    
    private void privateMethod() {
        System.out.println("Private method called");
    }
}

Reflection provides runtime class information. Can inspect methods, fields, constructors. Can create instances and invoke methods. Can access private members with setAccessible(true).

Chapter 13: Practical Implementation

13.1 Real-World Projects

Banking System

A banking system handles accounts, transactions, and customer management.

Account stores balance and type. Transaction records operations. Bank manages accounts. Encapsulation protects data. Validation ensures integrity.

Code Example

import java.util.*;
import java.time.*;

// ---- ACCOUNT CLASS ----
class Account {
    private int accountNumber;
    private String customerName;
    private double balance;
    private String type;
    private List<Transaction> transactions;
    private static int nextAccountNumber = 1000;
    
    public Account(String customerName, double initialBalance, String type) {
        this.accountNumber = nextAccountNumber++;
        this.customerName = customerName;
        this.balance = initialBalance;
        this.type = type;
        this.transactions = new ArrayList<>();
        addTransaction("Opening", initialBalance);
    }
    
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        balance += amount;
        addTransaction("Deposit", amount);
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
        addTransaction("Withdrawal", -amount);
    }
    
    private void addTransaction(String description, double amount) {
        transactions.add(new Transaction(description, amount, LocalDateTime.now()));
    }
    
    public int getAccountNumber() { return accountNumber; }
    public String getCustomerName() { return customerName; }
    public double getBalance() { return balance; }
    public String getType() { return type; }
    public List<Transaction> getTransactions() { return new ArrayList<>(transactions); }
    
    public void display() {
        System.out.println("Account #" + accountNumber + ": " + customerName);
        System.out.println("Type: " + type + ", Balance: $" + String.format("%.2f", balance));
        System.out.println("Transactions: " + transactions.size());
    }
}

// ---- TRANSACTION CLASS ----
class Transaction {
    private String description;
    private double amount;
    private LocalDateTime dateTime;
    
    public Transaction(String description, double amount, LocalDateTime dateTime) {
        this.description = description;
        this.amount = amount;
        this.dateTime = dateTime;
    }
    
    public void display() {
        System.out.printf("%s: $%.2f on %s%n", 
            description, amount, dateTime.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
    }
}

// ---- BANK CLASS ----
class Bank {
    private String name;
    private Map<Integer, Account> accounts;
    
    public Bank(String name) {
        this.name = name;
        this.accounts = new HashMap<>();
    }
    
    public Account createAccount(String customerName, double initialBalance, String type) {
        Account account = new Account(customerName, initialBalance, type);
        accounts.put(account.getAccountNumber(), account);
        System.out.println("Account created: #" + account.getAccountNumber());
        return account;
    }
    
    public Account findAccount(int accountNumber) {
        Account account = accounts.get(accountNumber);
        if (account == null) {
            throw new IllegalArgumentException("Account not found");
        }
        return account;
    }
    
    public void transfer(int fromAccount, int toAccount, double amount) {
        Account source = findAccount(fromAccount);
        Account target = findAccount(toAccount);
        
        source.withdraw(amount);
        target.deposit(amount);
        System.out.println("Transferred $" + amount + " from #" + fromAccount + " to #" + toAccount);
    }
    
    public void displayAllAccounts() {
        System.out.println("\n=== Bank: " + name + " ===");
        for (Account account : accounts.values()) {
            account.display();
            System.out.println();
        }
    }
}

public class BankingSystem {
    public static void main(String[] args) {
        System.out.println("=== Banking System ===");
        
        Bank bank = new Bank("City Bank");
        
        // Create accounts
        Account account1 = bank.createAccount("John Doe", 1000, "Savings");
        Account account2 = bank.createAccount("Jane Smith", 500, "Checking");
        Account account3 = bank.createAccount("Bob Johnson", 2000, "Savings");
        
        System.out.println();
        
        // Perform transactions
        account1.deposit(500);
        account1.withdraw(200);
        account2.deposit(1000);
        
        // Transfer
        bank.transfer(1000, 1001, 300);
        
        // Display
        bank.displayAllAccounts();
        
        // Display transactions
        System.out.println("Transactions for Account #1000:");
        for (Transaction t : account1.getTransactions()) {
            t.display();
        }
    }
}

Accounts store customer data and balance. Transactions record all operations. Bank manages accounts and transfers. Encapsulation protects account data.

Library Management System

A library management system tracks books, members, and borrowing.

Book stores book details. Member tracks borrowed books. Library manages lending operations. Relationships between books and members. Validation ensures rules are followed.

Code Example

import java.util.*;
import java.time.*;
import java.time.temporal.*;

// ---- BOOK CLASS ----
class Book {
    private int id;
    private String title;
    private String author;
    private String isbn;
    private boolean available;
    private static int nextId = 1;
    
    public Book(String title, String author, String isbn) {
        this.id = nextId++;
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.available = true;
    }
    
    public int getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getIsbn() { return isbn; }
    public boolean isAvailable() { return available; }
    public void setAvailable(boolean available) { this.available = available; }
    
    public void display() {
        System.out.printf("%d. %s by %s (ISBN: %s) - %s%n",
            id, title, author, isbn, available ? "Available" : "Borrowed");
    }
}

// ---- MEMBER CLASS ----
class Member {
    private int id;
    private String name;
    private String email;
    private List<Book> borrowedBooks;
    private static int nextId = 1;
    private static final int MAX_BOOKS = 3;
    
    public Member(String name, String email) {
        this.id = nextId++;
        this.name = name;
        this.email = email;
        this.borrowedBooks = new ArrayList<>();
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public List<Book> getBorrowedBooks() { return new ArrayList<>(borrowedBooks); }
    
    public boolean canBorrow() {
        return borrowedBooks.size() < MAX_BOOKS;
    }
    
    public void borrowBook(Book book) {
        if (!canBorrow()) {
            throw new IllegalStateException("Member has reached max books limit");
        }
        if (!book.isAvailable()) {
            throw new IllegalStateException("Book is not available");
        }
        borrowedBooks.add(book);
        book.setAvailable(false);
        System.out.println("Book borrowed: " + book.getTitle());
    }
    
    public void returnBook(Book book) {
        if (!borrowedBooks.contains(book)) {
            throw new IllegalArgumentException("Book was not borrowed by this member");
        }
        borrowedBooks.remove(book);
        book.setAvailable(true);
        System.out.println("Book returned: " + book.getTitle());
    }
    
    public void display() {
        System.out.println("Member #" + id + ": " + name + " (" + email + ")");
        System.out.println("Borrowed books: " + borrowedBooks.size() + "/" + MAX_BOOKS);
        for (Book book : borrowedBooks) {
            System.out.println("  - " + book.getTitle());
        }
    }
}

// ---- LIBRARY CLASS ----
class Library {
    private String name;
    private Map<Integer, Book> books;
    private Map<Integer, Member> members;
    
    public Library(String name) {
        this.name = name;
        this.books = new HashMap<>();
        this.members = new HashMap<>();
    }
    
    public void addBook(String title, String author, String isbn) {
        Book book = new Book(title, author, isbn);
        books.put(book.getId(), book);
        System.out.println("Added book: " + title);
    }
    
    public void addMember(String name, String email) {
        Member member = new Member(name, email);
        members.put(member.getId(), member);
        System.out.println("Added member: " + name);
    }
    
    public Book findBook(int id) {
        Book book = books.get(id);
        if (book == null) {
            throw new IllegalArgumentException("Book not found");
        }
        return book;
    }
    
    public Member findMember(int id) {
        Member member = members.get(id);
        if (member == null) {
            throw new IllegalArgumentException("Member not found");
        }
        return member;
    }
    
    public void borrowBook(int memberId, int bookId) {
        Member member = findMember(memberId);
        Book book = findBook(bookId);
        member.borrowBook(book);
    }
    
    public void returnBook(int memberId, int bookId) {
        Member member = findMember(memberId);
        Book book = findBook(bookId);
        member.returnBook(book);
    }
    
    public void displayBooks() {
        System.out.println("\n=== Books in " + name + " ===");
        for (Book book : books.values()) {
            book.display();
        }
    }
    
    public void displayMembers() {
        System.out.println("\n=== Members in " + name + " ===");
        for (Member member : members.values()) {
            member.display();
            System.out.println();
        }
    }
}

public class LibrarySystem {
    public static void main(String[] args) {
        System.out.println("=== Library Management System ===");
        
        Library library = new Library("Central Library");
        
        // Add books
        library.addBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0-7432-7356-5");
        library.addBook("1984", "George Orwell", "978-0-452-28423-4");
        library.addBook("To Kill a Mockingbird", "Harper Lee", "978-0-06-112008-4");
        library.addBook("The Catcher in the Rye", "J.D. Salinger", "978-0-316-76948-0");
        
        System.out.println();
        
        // Add members
        library.addMember("Alice Johnson", "alice@email.com");
        library.addMember("Bob Williams", "bob@email.com");
        
        System.out.println();
        
        // Display initial state
        library.displayBooks();
        library.displayMembers();
        
        System.out.println("\n=== Transactions ===");
        
        // Borrow books
        library.borrowBook(1, 1);  // Alice borrows The Great Gatsby
        library.borrowBook(1, 2);  // Alice borrows 1984
        library.borrowBook(2, 3);  // Bob borrows To Kill a Mockingbird
        
        System.out.println();
        
        // Display after borrowing
        library.displayBooks();
        library.displayMembers();
        
        System.out.println("\n=== Returns ===");
        
        // Return book
        library.returnBook(1, 1);  // Alice returns The Great Gatsby
        
        System.out.println();
        library.displayBooks();
    }
}

Books store bibliographic data. Members track borrowed books. Library manages all operations. Relationships ensure integrity. Validation prevents rule violations.

Final Advice

Java feels verbose at first because it forces you to be explicit about everything—but that explicitness is exactly what makes it such a powerful language for building large-scale, reliable applications. Every other language you learn after this one will feel easier, because you’ll already understand the fundamental concepts: static typing, object-oriented design, exception handling, and memory management.

Begin with the first stage today by running “Hello, World!” yourself on your computer through the terminal.. Then break it on purpose—remove a semicolon, forget the String[] args, misspell System.out.println—and read the resulting error message slowly and carefully instead of panicking.

That single habit, repeated consistently over weeks and months, is genuinely how every strong Java programmer built their foundation.

Use the AI prompts provided for each concept. They’re designed to give you code examples, clear explanations, and practical exercises. Copy them into your favorite AI assistant and work through the examples yourself. Write the code, run it, modify it, break it, and fix it.

Good luck, and welcome to Java. The journey is explicit, but the destination is worth it.

Scroll to Top