Index

Classes, Objects, and Members

Java Syntax

6.1 Defining Classes

In Java, a class is the fundamental building block of object-oriented programming. A class defines a blueprint for objects—each object created from a class can have its own data (fields) and behavior (methods).

Basic Class Structure

Here's a simple class definition:

public class Person {
    // Fields (also called instance variables)
    String name;
    int age;

    // Method
    void greet() {
        System.out.println("Hello, my name is " + name);
    }
}

Naming Conventions and File Structure

Multiple Classes per File

Only one public class is allowed per .java file, and it must match the filename. However, you can define other non-public (package-private) classes in the same file.

class Helper {
    void help() {
        System.out.println("Helping...");
    }
}

But in practice, each class is usually placed in its own file for clarity and maintainability.

Reflection

A class serves as a template—you don't use the class itself directly but create objects (instances) based on it. Defining a class with clear structure, good naming, and organized methods makes your code reusable, readable, and scalable as your program grows. Understanding how to properly define a class is a vital first step in mastering Java's object-oriented features.

Index

6.2 Creating Objects

In Java, objects are instances of classes. Once you've defined a class, you can create objects using the new keyword, which calls the class's constructor and allocates memory for the new instance.

Basic Object Instantiation

Here's how to create an object from a class and access its members:

public class Person {
    String name;
    int age;

    void greet() {
        System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person(); // Object creation using 'new'
        p1.name = "Alice";
        p1.age = 30;
        p1.greet(); // Output: Hi, I'm Alice and I'm 30 years old.
    }
}

Constructor Role

Constructors are special methods invoked during object creation. If you don't define one, Java provides a default constructor.

You can also define custom constructors to initialize values:

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

Then call it like:

Person p2 = new Person("Bob", 25);

Forgetting new

If you forget to use new, like:

Person p; 
p.name = "Error!";

You'll get a NullPointerException because p has not been initialized. Always instantiate with new before using the object.

Click to view full runnable Code

public class Person {
    String name;
    int age;

    // Custom constructor
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Default constructor (optional to declare, Java provides it if no other constructor exists)
    Person() {
        this.name = "Unknown";
        this.age = 0;
    }

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

    public static void main(String[] args) {
        // Using custom constructor
        Person p1 = new Person("Alice", 30);
        p1.printInfo();

        // Using default constructor
        Person p2 = new Person();
        p2.printInfo();

        // Uncommenting the following will cause NullPointerException at runtime
        /*
        Person p3; // Declared but not initialized
        p3.name = "Error!"; // NullPointerException
        */
    }
}

Reflection

Creating and using objects is the heart of Java programming. Constructors simplify initialization, and using object references promotes clean and reusable code. Mastering object creation helps you structure your programs around real-world entities.

Index

6.3 Instance and Static Fields

In Java, fields are variables declared within a class. These can be either instance fields (each object has its own copy) or static fields (shared across all objects of the class). Understanding the difference is essential for writing efficient and clear code.

Instance Fields

Instance fields are tied to individual objects. Each object created from a class gets its own unique copy of these fields.

public class Car {
    String model; // instance field
    int year;
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.model = "Toyota";
        car1.year = 2020;

        Car car2 = new Car();
        car2.model = "Honda";
        car2.year = 2022;

        System.out.println(car1.model); // Toyota
        System.out.println(car2.model); // Honda
    }
}

Here, car1 and car2 have their own separate model and year fields.

Static Fields

Static fields belong to the class itself and are shared among all instances.

public class Car {
    String model;
    int year;
    static int totalCars = 0; // static field

    Car() {
        totalCars++;
    }
}

public class Main {
    public static void main(String[] args) {
        new Car();
        new Car();
        System.out.println(Car.totalCars); // 2
    }
}

Notice that totalCars is accessed via the class name Car.totalCars, and not through an instance. Every time a new Car is created, the static field is updated globally.

Use Cases and Reflection

Use instance fields for data that varies between objects (like name, age, or model). Use static fields for:

Using static fields carefully helps conserve memory and avoids redundancy—but overusing them can reduce flexibility and break encapsulation. Always consider whether a field should represent object-specific state or shared class-level data.

Index

6.4 Instance and Static Methods

In Java, methods are blocks of code that define behavior. Like fields, methods can be either instance (belonging to an object) or static (belonging to the class). Understanding how to use each appropriately is key to writing well-structured Java code.

Instance Methods

An instance method is associated with an object. You must create an instance of the class to call the method.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        int result = calc.add(5, 3); // instance method call
        System.out.println(result);  // 8
    }
}

Here, add() is called on the calc object, and each object can have its own internal state that instance methods can work with.

Static Methods

A static method belongs to the class, not any instance. You can call it without creating an object.

public class MathUtils {
    public static int square(int x) {
        return x * x;
    }
}

public class Main {
    public static void main(String[] args) {
        int result = MathUtils.square(4); // static method call
        System.out.println(result);       // 16
    }
}

Static methods are often used for utility or helper functions.

Access Rules

public class Example {
    static void staticMethod() {
        // Cannot access instanceMethod() directly
    }

    void instanceMethod() {
        staticMethod(); // Valid
    }
}

When to Use What

Choosing the right method type improves encapsulation, performance, and clarity.

Index

6.5 Access Modifiers: public, private, protected

In Java, access modifiers control the visibility of classes, methods, and variables. They define how accessible a member is from other classes and packages. Java provides four main levels of access control:

public

A public member is accessible from anywhere in the program.

public class Example {
    public int number = 42;
}

If another class imports or refers to Example, it can access number directly.

private

A private member is only accessible within the same class.

public class Example {
    private int secret = 123;

    public int getSecret() {
        return secret;
    }
}

Other classes cannot directly access secret. Instead, controlled access is typically provided via public methods like getSecret(). This is a core part of encapsulation.

protected

A protected member is accessible:

package animals;

public class Animal {
    protected void speak() {
        System.out.println("Animal speaks");
    }
}
package zoo;

import animals.Animal;

public class Dog extends Animal {
    public void bark() {
        speak(); // Legal: subclass can access protected method
    }
}

Default (Package-Private)

If no modifier is used, the member is package-private, meaning it is accessible only within the same package.

class Example {
    int data = 100; // default/package-private
}

Access Summary Table

Modifier Same Class Same Package Subclass (Other Package) Other Packages
public
protected
(default)
private

Reflection

Choosing the correct access modifier is critical for encapsulation—a principle where internal details are hidden to protect and simplify object usage. It's best to start with private and only increase visibility as needed, following the principle of least privilege. This keeps your classes well-encapsulated, secure, and easier to maintain.

Index

6.6 Static Blocks and Initialization Blocks

In Java, you can perform complex setup tasks using initializer blocks, which are special code blocks that run automatically when a class or object is loaded or created. These blocks come in two types:

Static Initializer Block

A static block runs once, when the class is first loaded into memory. It's typically used to initialize static variables.

Syntax:

public class Example {
    static int staticValue;

    static {
        staticValue = 100;
        System.out.println("Static block executed");
    }
}

Execution Order:

Instance Initializer Block

An instance block runs every time an object is created, before the constructor.

Syntax:

public class Example {
    int instanceValue;

    {
        instanceValue = 42;
        System.out.println("Instance initializer executed");
    }

    public Example() {
        System.out.println("Constructor executed");
    }
}

Output:

Instance initializer executed  
Constructor executed

Execution Order:

Click to view full runnable Code

public class Example {
    static int staticValue;

    // Static block runs once when the class is loaded
    static {
        staticValue = 100;
        System.out.println("Static block executed");
    }

    int instanceValue;

    // Instance initializer block runs every time an object is created, before constructor
    {
        instanceValue = 42;
        System.out.println("Instance initializer executed");
    }

    public Example() {
        System.out.println("Constructor executed");
    }

    public static void main(String[] args) {
        System.out.println("Main started");

        // Creating first object
        Example ex1 = new Example();

        // Creating second object
        Example ex2 = new Example();
    }
}

Reflection

Use initializer blocks for logic that is shared across constructors or for complex static initialization that cannot be expressed with a simple assignment. However, use them sparingly—for most cases, static variables should be initialized where declared, and constructor logic should remain in constructors for clarity.

Initializer blocks can help reduce code duplication and organize setup logic, but overusing them can make your class harder to read. When in doubt, prefer clear constructors and static factory methods.

Index

6.7 Using this Keyword

In Java, the this keyword is a special reference to the current object—the instance whose method or constructor is being executed. It helps differentiate between instance variables and parameters or local variables, and supports constructor chaining and passing object references.

Disambiguating Fields with this

When a constructor or method parameter shares a name with an instance variable, this is used to clarify intent:

public class Book {
    private String title;

    public Book(String title) {
        this.title = title; // Assigns parameter to instance variable
    }
}

Without this, title = title; would refer to the parameter on both sides, leaving the field unchanged.

Constructor Chaining with this()

Java allows one constructor to call another in the same class using this(). This avoids repeating initialization logic:

public class Rectangle {
    int width, height;

    public Rectangle() {
        this(10, 20); // Calls the parameterized constructor
    }

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
}

Note: this() must be the first statement in a constructor.

Passing the Current Object

You can pass this as an argument to methods that expect an instance of the current class:

public class Printer {
    void printInfo(Book b) {
        System.out.println("Book title: " + b.title);
    }

    void start() {
        printInfo(this); // Passes current object
    }
}
Click to view full runnable Code

public class Main {

    public static void main(String[] args) {
        // Testing Book with this
        Book myBook = new Book("Effective Java");
        System.out.println("Book title: " + myBook.getTitle());

        // Testing Rectangle constructor chaining
        Rectangle rect1 = new Rectangle();
        System.out.println("Rectangle default: " + rect1.width + "x" + rect1.height);

        Rectangle rect2 = new Rectangle(5, 8);
        System.out.println("Rectangle custom: " + rect2.width + "x" + rect2.height);

        // Testing passing this
        Printer printer = new Printer(myBook);
        printer.start();
    }
}

class Book {
    private String title;

    public Book(String title) {
        this.title = title; // disambiguate instance variable and parameter
    }

    public String getTitle() {
        return title;
    }
}

class Rectangle {
    int width, height;

    public Rectangle() {
        this(10, 20); // constructor chaining: calls the parameterized constructor
    }

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
}

class Printer {
    private Book book;

    public Printer(Book book) {
        this.book = book;
    }

    void printInfo(Book b) {
        System.out.println("Printing book info: " + b.getTitle());
    }

    void start() {
        printInfo(this.book); // pass current object state (the book)
    }
}

Reflection

Using this enhances clarity and reduces ambiguity, especially when naming conventions overlap. It's essential in constructor chaining and useful when an object needs to refer to itself. However, overusing it can clutter code unnecessarily. Use it where it improves readability or is required by syntax, and let naming clarity handle the rest.

Index