Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to this comprehensive guide to Java, one of the most enduring and widely used programming languages in the world. Created in the mid-1990s, Java was built on the principle of "Write Once, Run Anywhere." This powerful idea means that a single Java program can execute on a vast array of devices and operating systems without needing to be rewritten, as long as a Java Virtual Machine (JVM) is present.

Why Learn Java?

Learning Java opens the door to numerous opportunities. Its syntax is known for being clean and readable, which makes it an excellent choice for those new to programming. The cross-platform nature of Java allows developers to write code that functions seamlessly across Windows, macOS, Linux, and even specialized embedded systems. Furthermore, Java boasts a massive and mature ecosystem, offering developers access to thousands of libraries, frameworks, and tools that can accelerate development. As a result, Java remains in high demand across the industry, powering everything from large-scale enterprise applications and Android mobile apps to sophisticated cloud-based platforms.

What You Will Learn

This book provides a structured path to mastering Java. You will begin by setting up a complete Java development environment. From there, you will learn to write and execute fundamental Java programs. The guide will walk you through essential concepts such as classes, objects, and methods, which form the bedrock of object-oriented programming. As you progress, you will explore more advanced features, including generics, the collections framework, and streams. To solidify your understanding, you will have opportunities to build practical, real-world projects.

Who This Book is For

This guide is designed for a diverse audience. If you are a complete beginner with no prior programming experience, you will find a clear, step-by-step introduction to the language. If you are a developer coming from another programming language, this book will help you quickly get up to speed with the nuances of the Java ecosystem. Finally, for students, this guide will provide a strong and practical foundation that complements academic coursework, as Java is a cornerstone of many computer science programs.

Next Steps

The journey begins with setting up your development environment and writing your very first Java program. Let's proceed to the "Getting Started" chapter to get everything up and running.

Getting Started

This chapter will guide you through setting up a streamlined Java development environment using Visual Studio Code (VS Code). By the end of this section, you will have a fully functional Java Development Kit (JDK), a VS Code installation, and the necessary extensions for a productive coding experience, including compiling and debugging.

1. Install Visual Studio Code

The first step is to install Visual Studio Code. You can download it from the official website at https://code.visualstudio.com/. Follow the installation instructions tailored to your operating system.

  • For Windows, this involves running the downloaded .exe installer.
  • On macOS, you will drag the VS Code application into your Applications folder.
  • For Linux users, VS Code is available through most distribution package managers.

2. Install the Java Development Kit (JDK)

To compile and run Java code, you need a Java Development Kit (JDK). You have the choice between Oracle JDK and OpenJDK, which is an open-source alternative favored by most developers. We recommend the OpenJDK builds from Adoptium, available at https://adoptium.net/.

Alternatively, you can find the Oracle JDK at https://www.oracle.com/java/technologies/downloads/.

The installation process varies by operating system.

Follow the provider instructions to install the JDK on your operating system.

To confirm that the JDK was installed correctly, open a terminal and execute the command java -version. You should see output that displays the JDK version:

openjdk version "21.0.8" 2025-07-15
OpenJDK Runtime Environment (Red_Hat-21.0.8.0.9-1) (build 21.0.8+9)
OpenJDK 64-Bit Server VM (Red_Hat-21.0.8.0.9-1) (build 21.0.8+9, mixed mode, sharing)

3. Set up Java in VS Code

With both VS Code and the JDK installed, the next step is to configure VS Code for Java development by adding extensions.

The most important is the Extension Pack for Java by Microsoft. This pack provides comprehensive support for Java, including language support, debugging, testing, and integration with build tools like Maven and Gradle. To install it, open the Extensions view in VS Code (with Ctrl+Shift+X or Cmd+Shift+X), search for "Extension Pack for Java", and click "Install".

4. Create a Simple Java Program

To verify that your setup is working, let's create a simple "Hello, World!" program. In VS Code, create a new folder named hello-java and, within it, a file named HelloWorld.java. Add the following code to the file:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

You can run this program directly in VS Code by pressing Ctrl+F5.

Alternatively, you can open a terminal within the hello-java folder and run the commands javac HelloWorld.java to compile the code, followed by java HelloWorld to execute it. In either case, you should see the output "Hello, World!" printed to the console.

Setting Up Your Development Environment

A well-configured development environment is crucial for a productive and enjoyable programming experience. This section provides recommendations for setting up your environment for Java development.

Keyboard Layout

While it may seem like a minor detail, your keyboard layout can have a significant impact on your coding efficiency. Many programming languages, including Java, frequently use special characters such as ~, [, ], {, }, \, and |.

On some international keyboard layouts, these characters can be difficult to access, often requiring key combinations. Because of this, many developers prefer to use a US keyboard layout for programming, as it provides direct access to these symbols.

If you find yourself struggling to type these characters, you might consider switching to a US layout or using a tool that allows you to easily remap your keyboard. This can help you write code more fluently and with less frustration.

Handling Accented Characters

For developers who write in languages that use accented characters, such as Italian, typing these characters on a US keyboard layout can be a challenge. Fortunately, there are tools available for all major operating systems to make this easier:

  • Windows: PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity. One of these utilities, Quick Accent, allows you to easily type accented characters by holding down the key for the character you want to accent and then pressing the spacebar.

  • Linux: Most Linux distributions provide a Compose Key. When you press the Compose Key, you can then type a sequence of characters to produce a special character. For example, to type é, you would press Compose, then ', then e.

  • macOS: macOS has a built-in feature for typing accented characters. Simply press and hold the key for the character you want to accent, and a menu will appear with a list of possible accented versions of that character.

Language Server Protocol (LSP)

The Language Server Protocol (LSP) is a protocol developed by Microsoft that defines a common way for editors and IDEs to communicate with language servers. A language server provides language-specific features like code completion, diagnostics, and navigation.

By using LSP, a single language server can be used by multiple editors and IDEs, which means that language support can be implemented once and be available in many different tools.

How LSP helps in development

When configured correctly, an LSP server for Java can provide a lot of powerful features in your editor of choice, like VS Code, Sublime Text, or Vim/Neovim. These features can significantly improve your development workflow.

Code Navigation

  • Go to Definition: Quickly jump to the definition of a class, method, or variable.
  • Find References: Find all references to a symbol in the codebase.
  • Symbol Search: Search for symbols in the current file or the entire workspace.

Documentation on Hover

  • Hover: Hovering over a symbol will show its documentation, including method signatures, parameter information, and Javadocs.

Quick Fixes and Refactoring

  • Diagnostics: The LSP server analyzes your code and provides diagnostics (errors and warnings) as you type.
  • Quick Fixes: For many diagnostics, the LSP server can suggest quick fixes that you can apply with a single click. For example, it can automatically add a missing import statement.
  • Refactoring: Perform basic refactoring operations like renaming a symbol.

LSP in VS Code

Visual Studio Code has great support for the Language Server Protocol. For Java development, the Extension Pack for Java by Microsoft provides a rich set of features powered by an LSP server. Once installed, you will get all the features mentioned above and more, right out of the box.

The LSP integration makes the development experience in a lightweight editor like VS Code much closer to that of a full-fledged IDE like IntelliJ IDEA or Eclipse.

Using LSP features in VS Code

Once you have the Extension Pack for Java installed and a Java project opened, you can use the LSP features as follows:

  • Go to Definition: Right-click on a symbol (e.g., a method or class name) and select "Go to Definition", or F12.
  • Find References: Right-click on a symbol and select "Find All References", or Shift+F12.
  • Symbol Search: Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P on Mac) and type @ to search for symbols in the current file, or # to search the workspace.
  • Hover: Simply move your mouse cursor over any symbol to see its documentation.
  • Quick Fixes: When you have an error or warning, a lightbulb icon will appear. Click on it to see the available quick fixes, or use the Ctrl+. (Cmd+. on Mac) shortcut.
  • Refactoring: Right-click on a symbol and select "Rename Symbol" (F2) to rename it across the project.

Java Basics

This chapter introduces the fundamental building blocks of the Java language, providing a foundation for writing your first programs.

Basic Syntax

At the core of every Java program is a class. The execution of a program begins in the main method, which serves as the entry point. It is important to be mindful of Java's syntax rules. The language is case-sensitive, meaning that myVariable and MyVariable would be treated as two distinct variables. By convention, class names should always begin with an uppercase letter, while method names should start with a lowercase letter.

Check out the Java naming convention to learn how to properly name things in Java.

"Hello, World!" Example

The simplest program you can write in Java is the classic "Hello, World!". This program's sole purpose is to print the phrase "Hello, World!" to the console.

public class HelloWorld {
    public static void main(String[] args) {
        // This line prints "Hello, World!"
        System.out.println("Hello, World!");
    }
}

Let's break down this example:

  • The line public class HelloWorld declares a class named HelloWorld, and the file containing this code must be saved as HelloWorld.java.
  • The main method, declared as public static void main(String[] args), is where the program's execution starts.
  • The statement System.out.println("Hello, World!"); is responsible for printing the text to the console.
  • The lines starting with // are single-line comments, which are ignored by the compiler.

Java Comments

Comments are essential for explaining code and improving its readability. The Java compiler ignores them entirely.

There are three types of comments in Java. Single-line comments start with //. Multi-line comments begin with /* and end with */. Finally, Javadoc comments, which start with /** and end with */, are used to generate documentation for your code.

// This is a single-line comment.

/*
This is a
multi-line
comment.
*/

/**
 * This is a Javadoc comment, used for generating documentation.
 */

Variables in Java

In Java, variables are fundamental containers used for storing data. Every variable must have a declared type, which dictates the kind of values it can hold.

Primitive Types

Java provides eight primitive data types, which are the most basic building blocks for data manipulation. These include byte, short, int, and long for integer values of increasing size. For decimal numbers, Java offers float and double. The char type is used for single Unicode characters, and the boolean type can only hold the values true or false.

Here are a few examples of declaring and initializing variables with primitive types:

int age = 25;
double price = 19.99;
// Note: boolean variables are often prefixed with `is`, `has`, etc.
// to clearly indicate the purpose.
boolean isJavaFun = true;
char grade = 'A';

Reference Types

In addition to primitive types, Java has reference types, which are used to refer to objects. The most common reference type is String, which is used for sequences of characters. Unlike primitive types, reference types are typically created with the new keyword, although String has special support that allows for simpler initialization.

String name = "Mary";

Declaring and Initializing

A variable can be declared first and then assigned a value later in the program. For instance, you can declare an integer variable number and then initialize it with the value 42 on a separate line. Alternatively, it is often more convenient to declare and initialize a variable in a single step.

// Declaration and initialization in two steps
int number;
number = 42;

// Declaration and initialization in one step
int anotherNumber = 42;

Type Casting

There are times when you need to convert a value from one data type to another. This process is known as type casting. A "widening" conversion, where you convert to a larger data type (e.g., from int to double), is considered safe and is performed automatically by Java. However, a "narrowing" conversion, where you might lose information (e.g., from double to int), requires an explicit cast to inform the compiler that you are aware of the potential data loss.

// Widening conversion (automatic)
int x = 10;
double y = x;  // The int value is safely converted to a double

// Narrowing conversion (explicit cast required)
double a = 9.78;
int b = (int) a;  // The double is truncated to an int, resulting in b = 9

Variable Scope

In Java, the scope of a variable determines where it can be accessed in your code. There are three main types of variable scope:

  • Local Variables: These are declared within a method or a block of code. They can only be accessed within that specific method or block.

  • Instance Variables: These are declared within a class but outside of any method. They are associated with an instance of the class (an object) and can be accessed by any method of that instance.

  • Class (or Static) Variables: These are declared with the static keyword and are associated with the class itself, not with any specific instance. They can be accessed from anywhere in the class.

Understanding variable scope is crucial for writing well-structured and bug-free Java code.

Control Flow in Java

Control flow statements are essential tools in Java that allow you to dictate the execution path of your code. These statements enable you to run certain blocks of code based on specific conditions and to repeat actions using loops.

Conditional Statements

Conditional statements allow a program to make decisions. The if-else statement is the most common conditional structure. It executes a block of code if a specified condition is true, and a different block if it is false. You can also chain multiple conditions using else if.

int number = 10;

if (number > 0) {
    System.out.println("The number is positive.");
} else if (number < 0) {
    System.out.println("The number is negative.");
} else {
    System.out.println("The number is zero.");
}

Another useful conditional statement is the switch statement, which is ideal for situations where you need to test a variable against a series of possible values.

int day = 3;
String dayString;

switch (day) {
    case 1:  dayString = "Monday";
             break;
    case 2:  dayString = "Tuesday";
             break;
    case 3:  dayString = "Wednesday";
             break;
    default: dayString = "An other day";
             break;
}
System.out.println(dayString); // Outputs "Wednesday"

Loops

Loops are used to execute a block of code repeatedly. The for loop is commonly used when you know in advance how many times you want to iterate.

for (int i = 0; i < 5; i++) {
    System.out.println("Iteration: " + i);
}

Similar to the for loop, there is the for-each that let you iterate over a whole collection.

// Collection like `List` will be explained later in this guide
List<String> names = List.of("John", "Jacob", "Mary");

for (String name : names) {
    System.out.println("Name: " + name);
}

The while loop is suitable for situations where you want to continue iterating as long as a certain condition remains true.

int i = 0;
while (i < 5) {
    System.out.println("Iteration: " + i);
    i++;
}

A variation of the while loop is the do-while loop, which guarantees that the code block is executed at least once, as the condition is checked at the end of the iteration.

int j = 0;
do {
    System.out.println("Iteration: " + j);
    j++;
} while (j < 5);

Break and Continue

Within loops, you can use the break statement to exit the loop entirely, or the continue statement to skip the current iteration and proceed to the next one.

for (int i = 0; i < 5; i++) {
    if (i == 2) {
        continue;  // Skip the rest of the code for i = 2
    }
    if (i == 4) {
        break;     // Exit the loop when i = 4
    }
    System.out.println(i);
}

Methods

In Java, a method is a block of code designed to perform a specific task. Methods are declared within a class and are executed when they are called, or invoked. They are a cornerstone of well-structured and reusable code.

Defining a Method

A method definition includes several components. It starts with an access modifier, such as public or private, which controls the method's visibility. This is followed by the return type, which specifies the data type of the value the method will return. If the method does not return any value, the void keyword is used. The method name is a unique identifier for the method. Optionally, a method can accept a list of input values, known as parameters. The code to be executed is contained within the method body.

public class MyClass {

    public void greet(String name) {
        // Method body
        System.out.println("Hello, " + name);
    }
}

Calling a Method

To execute a method, you typically need to create an object of the class to which the method belongs, unless the method is declared as static. Once you have an object, you can call the method on it.

public class Main {
    public static void main(String[] args) {
        MyClass myObject = new MyClass(); // Create an instance of MyClass
        myObject.greet("George");         // Call the greet method
    }
}

Parameters

Parameters are variables that allow you to pass input values to a method. This makes methods more flexible and reusable. For example, a method to add two numbers would accept two integer parameters.

public int add(int a, int b) {
    return a + b; // Returns the sum of the two parameters
}

// To call this method:
int sum = add(5, 10); // The variable 'sum' will be assigned the value 15

Variable-Length Arguments (Varargs)

Sometimes, you may not know in advance how many arguments a method will need to accept. Java provides a feature called varargs that allows a method to accept a variable number of arguments of the same type. This is indicated by an ellipsis (...) after the data type of the parameter.

public int sum(int... numbers) {
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return total;
}

// You can call this method with any number of integer arguments:
int result1 = sum(1, 2, 3);       // result1 will be 6
int result2 = sum(10, 20, 30, 40); // result2 will be 100

Pass by Value vs. Pass by Reference

When passing arguments to methods, it's important to understand that Java is strictly a pass-by-value language. This means that when a method is called, a copy of the value of each argument is passed to the method's parameters.

  • For primitive types (like int, double, boolean), the value itself is copied. Any changes made to the parameter inside the method will not affect the original variable outside the method.

  • For object types, the value being passed is the reference to the object, not the object itself. This reference is also passed by value, meaning a copy of the reference is created. As a result, the method can modify the object's internal state, and these changes will be reflected outside the method. However, if the method reassigns the parameter to a new object, this will not affect the original object reference.

Return Values

Methods can return a value to the caller using the return keyword. The data type of the returned value must match the method's declared return type. If a method is declared with a void return type, it does not return a value and the return keyword is used only to exit the method.

// This method returns a String
public String getGreeting() {
    return "Hello, World!";
}

// This method does not return a value
public void printMessage() {
    System.out.println("This is a message.");
}

Classes and Objects

Classes and objects are the foundational concepts of Object-Oriented Programming (OOP). A class can be thought of as a blueprint for creating objects. It defines a set of properties, known as fields, and behaviors, known as methods, that will be common to all objects of that type. An object, in turn, is a specific instance of a class.

Defining a Class

To define a class, you use the class keyword. For example, here is a simple class that serves as a template for creating Dog objects. It includes fields for the dog's breed, age, and color, as well as a method for the behavior of barking.

public class Dog {
    // Fields (properties) of the Dog class
    String breed;
    int age;
    String color;

    // Method (behavior) of the Dog class
    void bark() {
        System.out.println("Woof!");
    }
}

Creating an Object

Once a class is defined, you can create objects, or instances, of that class using the new keyword. After creating an object, you can access its fields and methods. For example, you can create a Dog object and then set its breed, age, and color, and then call its bark method.

public class Main {
    public static void main(String[] args) {
        // Create a new Dog object
        Dog myDog = new Dog();

        // Access and set the fields of the myDog object
        myDog.breed = "Golden Retriever";
        myDog.age = 3;
        myDog.color = "Golden";

        // Call the bark method on the myDog object
        myDog.bark(); // This will print "Woof!" to the console
    }
}

Constructors

A constructor is a special type of method that is automatically called when an object is created. Its primary purpose is to initialize the object's fields. A constructor must have the same name as the class and cannot have a return type.

public class Dog {
    String breed;
    int age;

    // This is the constructor for the Dog class
    public Dog(String dogBreed, int dogAge) {
        breed = dogBreed;
        age = dogAge;
    }

    public static void main(String[] args) {
        // Create a new Dog object using the constructor
        Dog anotherDog = new Dog("Labrador", 5);

        // Access the fields of the anotherDog object
        System.out.println(anotherDog.breed); // Prints "Labrador"
        System.out.println(anotherDog.age);   // Prints "5"
    }
}

Enums

In Java, an enum is a special data type that represents a fixed set of constant values. Enums are useful when you have a variable that can only take one of a small set of possible values, such as the days of the week or the seasons of the year.

Defining an Enum

To define an enum, you use the enum keyword. The enum values are listed in uppercase letters.

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

Using an Enum

You can use an enum in a switch statement to execute different code for each enum value.

public class EnumExample {
    public static void main(String[] args) {
        Day today = Day.WEDNESDAY;

        switch (today) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;
            case FRIDAY:
                System.out.println("Fridays are better.");
                break;
            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;
            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }
}

Java Core Concepts Wrap-up

This section covered the fundamental building blocks of the Java programming language. You learned about:

  • Variables: How to declare and initialize variables of different data types.
  • Control Flow: How to use if, else, switch, for, and while to control the flow of your program.
  • Methods: How to define and call methods to organize your code.
  • Classes and Objects: The basics of object-oriented programming in Java.
  • Enums: How to use enums to define a set of constants.

Exercise

Write a simple Java program that simulates a basic calculator. The program should be able to perform addition, subtraction, multiplication, and division.

Requirements:

  1. Create a Calculator class.
  2. The Calculator class should have methods for add, subtract, multiply, and divide.
  3. Create a Main class with a main method to test your Calculator.
  4. In the main method, create an instance of the Calculator class and call its methods with some sample numbers.
  5. Print the results of the calculations to the console.
  6. Use a switch statement to select the operation to perform based on a variable.

Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data in the form of fields (often known as attributes or properties) and code in the form of procedures (often known as methods).

This section will introduce you to the core principles of OOP in Java. We will cover:

  • Classes and Objects: The fundamental building blocks of OOP.
  • Inheritance: How to create new classes from existing ones.
  • Interfaces: How to achieve abstraction and multiple inheritance in Java.

By the end of this section, you will have a solid understanding of how to structure your Java programs in an object-oriented way.

Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows a new class, known as a subclass, to inherit the properties and behaviors (fields and methods) of an existing class, referred to as a superclass. This powerful mechanism is a cornerstone of code reuse, as it allows you to build upon existing, proven code.

Superclass and Subclass

The two key components of inheritance are the superclass (or parent class), which is the class being inherited from, and the subclass (or child class), which is the class that inherits from the superclass. The relationship between them is established using the extends keyword.

Example of Inheritance

To illustrate this, let's consider a Vehicle superclass. This class has a brand field and a honk method.

public class Vehicle {
    String brand;

    public void honk() {
        System.out.println("Tuut, tuut!");
    }
}

Now, we can create a Car subclass that extends Vehicle. The Car class will automatically inherit the brand field and the honk method from its superclass.

public class Car extends Vehicle {
    String modelName;

    public static void main(String[] args) {
        Car myCar = new Car();

        // The 'brand' field is inherited from the Vehicle class
        myCar.brand = "Ford";
        myCar.modelName = "Mustang";

        // The 'honk' method is also inherited from Vehicle
        myCar.honk();

        System.out.println(myCar.brand + " " + myCar.modelName);
    }
}

The super Keyword

The super keyword is a special keyword in Java that provides a reference to the superclass. It can be used in two primary ways: to call a constructor of the superclass and to access members (fields or methods) of the superclass. This is particularly useful when you need to explicitly invoke the superclass's implementation of a method or constructor from within the subclass.

Calling the Superclass Constructor

A common use of super is to call the constructor of the superclass from within the constructor of the subclass. This ensures that the superclass is properly initialized before the subclass.

public class Vehicle {
    String brand;

    // Constructor for the Vehicle superclass
    public Vehicle(String brand) {
        this.brand = brand;
    }
}

public class Car extends Vehicle {
    String modelName;

    // Constructor for the Car subclass
    public Car(String brand, String model) {
        // Call the constructor of the superclass (Vehicle)
        super(brand);
        this.modelName = model;
    }
}

The instanceof Operator

The instanceof operator is used to check whether an object is an instance of a particular class or a subclass of that class. It returns a boolean value, which is true if the object is an instance of the specified class or any of its subclasses, and false otherwise.

public class Main {
    public static void main(String[] args) {
        Vehicle myCar = new Car("Ford", "Mustang");

        // Check if myCar is an instance of Car
        System.out.println(myCar instanceof Car); // true

        // Check if myCar is an instance of Vehicle
        System.out.println(myCar instanceof Vehicle); // true
    }
}

Interfaces

In Java, an interface is a reference type that is similar to a class but is a collection of abstract methods. An interface defines a "contract" that other classes can agree to follow. When a class implements an interface, it must provide an implementation for all the methods defined in that interface.

Defining an Interface

An interface is defined using the interface keyword. The methods declared in an interface are abstract by default, meaning they do not have a body. Similarly, any attributes declared in an interface are automatically public, static, and final.

interface Animal {
    // These methods are implicitly public and abstract
    void emitSound();
    void sleep();
}

Implementing an Interface

A class can implement an interface using the implements keyword. When a class implements an interface, it must provide a concrete implementation for all the methods declared in that interface. This ensures that the class adheres to the contract defined by the interface.

// The Pig class implements the Animal interface
class Pig implements Animal {
    // Provide an implementation for the emitSound method
    public void emitSound() {
        System.out.println("The pig says: wee wee");
    }

    // Provide an implementation for the sleep method
    public void sleep() {
        System.out.println("Zzz");
    }
}

class Main {
    public static void main(String[] args) {
        Pig myPig = new Pig();
        myPig.emitSound();
        myPig.sleep();
    }
}

Key Differences Between an Interface and a Class

Interfaces and classes have some fundamental differences. An interface cannot be instantiated, meaning you cannot create an object of an interface type. Also, as mentioned, the methods in an interface do not have a body. A key advantage of interfaces is that a class can implement multiple interfaces, whereas it can only extend a single superclass.

Why Use Interfaces?

Interfaces are a powerful feature in Java for several reasons. They are a key mechanism for achieving abstraction, by separating the definition of a method from its implementation. They also provide a way to achieve a form of multiple inheritance, as a class can implement any number of interfaces. Finally, interfaces help in achieving loose coupling, making your code more modular and easier to maintain.

Abstract Classes

In Java, an abstract class is a class that cannot be instantiated on its own and must be subclassed. It serves as a blueprint for other classes, providing a common interface and shared implementation. Abstract classes are declared with the abstract keyword.

Abstract Methods

An abstract class can contain abstract methods, which are methods that are declared without an implementation. Subclasses are required to provide an implementation for these methods.

public abstract class Animal {
    // Abstract method (does not have a body)
    public abstract void makeSound();

    // Regular method
    public void sleep() {
        System.out.println("Zzz");
    }
}

Subclassing an Abstract Class

To use an abstract class, you must create a subclass that extends it. The subclass must provide an implementation for all of the abstract methods in the parent class.

public class Dog extends Animal {
    public void makeSound() {
        System.out.println("Woof");
    }
}

Packages

In Java, packages are used to group related classes and interfaces into a single namespace. They are analogous to folders in a file directory and are a crucial mechanism for organizing code, preventing naming conflicts, and improving code maintainability.

Creating a Package

To place a class within a package, you use the package keyword at the very beginning of your Java source file. The package name typically follows a hierarchical naming pattern. For example, to create a package named com.example.myapp, you would have a corresponding directory structure src/com/example/myapp, and the Java files within that directory would start with the package com.example.myapp; declaration.

// This file would be located at src/com/example/myapp/MyClass.java
package com.example.myapp;

public class MyClass {
    // ...
}

Using a Class from Another Package

To use a class that resides in a different package, you must first import it using the import keyword. You have the option to import a single, specific class or all the classes within a package.

Importing a Single Class

To import a specific class, you provide its fully qualified name in the import statement.

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner myScanner = new Scanner(System.in);
        // ...
    }
}

Importing an Entire Package

If you need to use multiple classes from the same package, Java allows to import all of them at once using the * wildcard, although it is considered bad practice.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Scanner myScanner = new Scanner(System.in);
        // ...
    }
}

Built-in Packages

Java provides a rich standard library organized into numerous built-in packages. Some of the most frequently used packages include java.lang, which contains fundamental classes and is automatically imported into every Java program; java.util, which provides the collections framework and other utility classes; java.io for input and output operations; java.net for networking; and java.awt for creating graphical user interfaces.

Object-Oriented Programming Wrap-up

This section covered the core principles of Object-Oriented Programming (OOP) in Java. You learned about:

  • Inheritance: How to create new classes that inherit properties and methods from existing classes.
  • Interfaces: How to define contracts that classes can implement.
  • Abstract Classes: How to create classes that cannot be instantiated and must be subclassed.
  • Packages: How to organize your classes into packages.

Exercise

Write a Java program that models a simple zoo.

Requirements:

  1. Create an Animal interface with a speak() method.
  2. Create an abstract class Mammal that implements the Animal interface. The Mammal class should have a name property.
  3. Create two concrete classes, Dog and Cat, that extend the Mammal class. Implement the speak() method in each class to print "Woof!" for the dog and "Meow!" for the cat.
  4. Create a Zoo class that has a list of Animals. Add a method to the Zoo class to add new animals to the list.
  5. Create a Main class with a main method to test your classes. In the main method, create a Zoo instance, add a Dog and a Cat to it, and then iterate through the animals in the zoo and call their speak() method.

Advanced Topics

This section delves into more advanced topics in Java that are essential for building robust and scalable applications. We will cover a range of concepts that will help you write more professional and efficient code.

In this section, you will learn about:

  • Exceptions: How to handle errors and exceptional situations in your programs.
  • Collections: The Java Collections Framework for working with groups of objects.
  • Streams: A powerful API for processing sequences of elements.
  • File I/O: How to read from and write to files.
  • Threads: How to work with threads for concurrent programming.
  • Generics: How to write flexible and reusable code with generics.

By mastering these topics, you will be well-equipped to tackle more complex programming challenges in Java.

Exceptions

In Java, an exception is an event that occurs during the execution of a program and disrupts its normal flow. When an error is encountered within a method, the method creates an exception object, which contains information about the error, and hands it off to the runtime system.

Exception Handling

Java provides a robust mechanism for handling exceptions using the try, catch, and finally keywords. This allows you to gracefully manage errors and prevent your program from crashing.

The try-catch Block

The try block is used to enclose a section of code that might throw an exception. If an exception occurs within the try block, the program looks for a corresponding catch block to handle it. The catch block contains the code that is executed to manage the exception.

public class Main {
    public static void main(String[] args) {
        try {
            // This code may cause an exception
            int[] myNumbers = {1, 2, 3};
            System.out.println(myNumbers[10]); // This will throw an ArrayIndexOutOfBoundsException
        } catch (ArrayIndexOutOfBoundsException e) {
            // This code handles the exception
            System.out.println("An error occurred: the index is out of bounds.");
            System.out.println("Error message: " + e.getMessage());
        }
    }
}

The finally Block

The finally block is an optional block that is always executed, regardless of whether an exception was thrown or caught. It is typically used for cleanup operations, such as closing files or releasing database connections, to ensure that resources are properly managed.

public class Main {
    public static void main(String[] args) {
        try {
            int[] myNumbers = {1, 2, 3};
            System.out.println(myNumbers[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("An error occurred.");
        } finally {
            System.out.println("The 'try-catch' block has finished execution.");
        }
    }
}

Try With Resources

finally blocks are often used to handle cleanup operations. For example, it's common to close a resource, but this can lead to verbose and nested code, especially when the cleanup operation itself can throw an exception.

The try-with-resources statement, introduced in Java 7, simplifies this process by ensuring that each resource is closed at the end of the statement. A resource is an object that must be closed after the program is finished with it, such as a file or a database connection.

Any object that implements the java.lang.AutoCloseable interface can be used as a resource.

Here is an example of using try-with-resources to read from a file:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("An I/O error occurred: " + e.getMessage());
        }
    }
}

In this example, the BufferedReader is declared within the try statement. It is automatically closed when the try block is exited, whether normally or due to an exception. This makes the code more concise and readable, and it prevents resource leaks.

Common Exception Types

Java has a wide range of built-in exception types. Some of the most common ones you will encounter include ArrayIndexOutOfBoundsException, which occurs when you try to access an array with an invalid index; NullPointerException, which is thrown when you try to use a variable that is not pointing to any object; ArithmeticException, which indicates an exceptional arithmetic condition, such as dividing by zero; and FileNotFoundException, which is thrown when a specified file cannot be found.

Custom exceptions can be created to handle domain-specific errors related to the software written.

Collections

The Java Collections Framework is a sophisticated hierarchy of interfaces and classes designed to help you manage and represent collections of objects. It provides a unified architecture for storing and manipulating groups of data, offering a wide range of data structures to suit different needs.

ArrayList

The ArrayList is a dynamic, resizable array and is one of the most frequently used classes in the Collections Framework. It provides a flexible way to store and manipulate a list of elements.

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> cars = new ArrayList<>();
        cars.add("Volvo");
        cars.add("BMW");
        cars.add("Ford");

        System.out.println(cars); // [Volvo, BMW, Ford]

        // You can access elements by their index
        System.out.println(cars.get(0)); // Retrieves "Volvo"

        // Elements can be modified
        cars.set(0, "Opel");

        // Elements can be removed
        cars.remove(0);

        // The size of the list can be retrieved
        System.out.println(cars.size()); // Outputs 2

        // You can iterate through the list
        for (String car : cars) {
            System.out.println(car);
        }

        // The list can be sorted
        Collections.sort(cars);
        System.out.println(cars);
    }
}

HashMap

A HashMap is a data structure that stores items in key-value pairs. This allows for efficient retrieval of a value if you know its corresponding key.

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, String> capitalCities = new HashMap<>();

        // Add key-value pairs to the map
        capitalCities.put("England", "London");
        capitalCities.put("Germany", "Berlin");
        capitalCities.put("Norway", "Oslo");

        System.out.println(capitalCities);

        // Access an item by its key
        System.out.println(capitalCities.get("England")); // Outputs "London"

        // Remove an item by its key
        capitalCities.remove("England");

        // You can loop through the keys of the map
        for (String country : capitalCities.keySet()) {
            System.out.println("The capital of " + country + " is " + capitalCities.get(country));
        }
    }
}

HashSet

A HashSet is a collection that stores only unique items. It is very efficient for checking if an item is present in the set.

import java.util.HashSet;

public class Main {
    public static void main(String[] args) {
        HashSet<String> cars = new HashSet<>();
        cars.add("Volvo");
        cars.add("BMW");
        cars.add("Ford");
        cars.add("BMW"); // This duplicate entry will be ignored

        System.out.println(cars); // [Volvo, BMW, Ford]

        // Check if an item exists in the set
        System.out.println(cars.contains("Volvo")); // true

        // Remove an item from the set
        cars.remove("Volvo");
    }
}

Streams

A Stream in Java is a powerful construct that represents a sequence of objects and supports a variety of operations that can be pipelined to achieve a desired result. Streams are a declarative way to process data, allowing you to focus on what you want to achieve rather than how to do it.

Understanding Streams

A Stream is not a data structure itself, but rather a sequence of elements that are processed in a pipeline of operations. Streams take their input from sources like Collections, Arrays, or I/O resources. They support a rich set of aggregate operations, such as filter, map, reduce, and collect, which can be chained together to form a processing pipeline. A key feature of streams is that they handle iteration automatically, allowing you to write more concise and readable code.

Creating Streams

You can easily create a stream from an existing collection, such as a List or a Set.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();

Common Stream Operations

The real power of streams lies in their operations. The forEach method provides a simple way to iterate over the elements of a stream.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);

The map operation allows you to transform each element of a stream into a new element. For example, you could use map to create a new list containing the squares of a list of numbers.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
                                .map(n -> n * n)
                                .collect(Collectors.toList());
// The 'squares' list will contain [1, 4, 9, 16, 25]

The filter operation is used to select elements from a stream that satisfy a given condition. For instance, you could filter a list of names to keep only those that start with the letter 'A'.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna");
List<String> aNames = names.stream()
                           .filter(name -> name.startsWith("A"))
                           .collect(Collectors.toList());
// The 'aNames' list will contain ["Alice", "Anna"]

Finally, the reduce operation combines the elements of a stream into a single result. A common use case for reduce is to calculate the sum of a list of numbers.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);
// The 'sum' variable will hold the value 15

Generics

Generics in Java allow you to create classes, interfaces, and methods that can work with any data type. This provides compile-time type safety and reduces the need for type casting.

Generic Class

You can create a generic class by specifying a type parameter in angle brackets (<>).

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

Using a Generic Class

When you create an instance of a generic class, you specify the actual type that the generic type will be replaced with.

public class Main {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(10);
        Integer someInteger = integerBox.get();
        System.out.println(someInteger);

        Box<String> stringBox = new Box<String>();
        stringBox.set("Hello World");
        String someString = stringBox.get();
        System.out.println(someString);
    }
}

File I/O

File I/O (Input/Output) in Java is used to read from and write to files. The java.io package provides a set of classes for handling file operations.

Reading a File

You can use the BufferedReader class to read a file line by line.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

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

Writing to a File

You can use the BufferedWriter class to write to a file.

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class WriteFileExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, World!");
            writer.newLine();
            writer.write("This is a new line.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Threads

Threads allow a program to perform multiple tasks concurrently. In Java, you can create threads by extending the Thread class or implementing the Runnable interface.

Extending the Thread Class

You can create a new thread by creating a subclass of Thread and overriding the run() method.

public class MyThread extends Thread {
    public void run() {
        System.out.println("This code is running in a thread");
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

Implementing the Runnable Interface

The preferred way to create a thread is to implement the Runnable interface. This allows your class to extend other classes.

public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("This code is running in a thread");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

Advanced Java Wrap-up

This section covered some of the more advanced topics in Java. You learned about:

  • Exceptions: How to handle errors and exceptional situations in your code.
  • Collections: How to use the Java Collections Framework to store and manipulate groups of objects.
  • Streams: How to use the Streams API to process collections of data in a declarative way.
  • Generics: How to write type-safe code that works with different types of objects.
  • File I/O: How to read from and write to files.
  • Threads: How to write multithreaded programs that can perform multiple tasks concurrently.

Exercise

Write a Java program that reads a list of words from a file, counts the frequency of each word, and then writes the results to a new file.

Requirements:

  1. Create a text file named words.txt with a list of words, one word per line.
  2. Write a program that reads the words from words.txt.
  3. Use a Map to store the frequency of each word.
  4. Use the Streams API to process the list of words.
  5. Write the word frequencies to a new file named word_frequencies.txt.
  6. Handle any potential IOExceptions that may occur.

Build Tools and Version Control

In addition to the Java language itself, there are two other essential tools that every modern Java developer should know: a build tool and a version control system.

Maven: The Build Tool

Maven is a powerful build automation tool that is used to manage the lifecycle of a Java project. This includes compiling the source code, running tests, and packaging the application into a distributable format, such as a JAR file.

Why Use a Build Tool?

In the early days of Java, developers had to manually manage the libraries and dependencies that their projects needed. This often led to a problem known as "JAR hell," where it was difficult to manage the correct versions of all the different JAR files that a project needed.

Build tools like Maven solve this problem by automating the process of dependency management. You simply declare the dependencies that your project needs in the pom.xml file, and Maven will automatically download the correct versions of the JAR files and make them available to your project.

This has several advantages:

  • Consistency: Everyone on the team uses the same versions of the dependencies.
  • Transitive Dependencies: Maven automatically handles transitive dependencies, which are the dependencies of your dependencies.
  • Reproducibility: The build process is reproducible, meaning that you can be confident that your project will build correctly on any machine.

However, there are also some potential disadvantages to be aware of:

  • Transitive Dependency Conflicts: Sometimes, different dependencies can require different versions of the same transitive dependency. This can lead to conflicts that need to be resolved manually.
  • Complexity: For very simple projects, using a build tool like Maven can add unnecessary complexity.

A Simple Maven Project

A Maven project is defined by a file called pom.xml, which is located in the root directory of the project. This file describes the project, its dependencies, and how to build it.

Here is an example of a simple pom.xml file:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Building a Project with Maven

To build a project with Maven, you can use the following commands:

CommandDescription
mvn compileCompiles the source code of the project.
mvn testRuns the tests for the project.
mvn packageCompiles the source code, runs the tests, and packages the application into a JAR file in the target directory.
mvn cleanCleans the project by deleting the target directory.
mvn installDeploys the package into the local repository, for use as a dependency in other projects locally.

You can also combine commands, for example:

mvn clean package

This command will first clean the project and then build it.

To learn more about Maven, checkout the official documentation on maven.apache.org.

Git: The Version Control System

Git is a distributed version control system that is used to track changes to source code over time. It allows multiple developers to work on the same project simultaneously without overwriting each other's changes.

A Simple Git Workflow

Here is a simple workflow for using Git:

  1. Initialize a repository: To start tracking a project with Git, you first need to initialize a repository in the root directory of the project:

    git init
    
  2. Add files to the staging area: Next, you need to add the files that you want to track to the staging area:

    git add .
    
  3. Commit the changes: Once the files are in the staging area, you can commit them to the repository:

    git commit -m "Initial commit"
    

Getting others' modifications

When you are working in a team, you will need to get the modifications made by other developers. To do this, you can use the git pull command. This command will fetch the changes from the remote repository and merge them into your local branch.

git pull

Sometimes, Git will not be able to merge the changes automatically. This can happen if you and another developer have modified the same lines in the same file. In this case, Git will create a conflict. You will need to resolve the conflict manually by editing the file and then committing the changes.

To learn more about Git, checkout the Pro Git Book on git-scm.com.

Useful Resources

This chapter provides a list of useful resources to help you continue your journey with Java.

Official documentation

Other sources

  • Baeldung: A popular blog with a wealth of tutorials and articles on Java and related technologies.

Contribute

If you find any issue with this guide, feel free to submit an issue on GitHub and get it solved.