Introduction to Classes & Objects in Java

In Java, object-oriented programming (OOP) allows developers to model real-world entities and their interactions. At the heart of OOP lies the class. A class serves as a blueprint for creating objects, which encapsulate both state and behavior.

Imagine you’re building a student management system. Instead of writing scattered, unorganized code, you can create modular, reusable components like Student, Course, and Teacher classes. This modularity makes the code easier to maintain, scale, and debug.

By understanding classes and objects, you can write clean and organized Java programs.

Definition of a Class

A class in Java is a blueprint for creating objects. It defines the properties (fields) and behaviors (methods) that the objects will possess.

Syntax of a Class

Here’s the basic structure of a class in Java:

class ClassName {
    // Fields (Instance Variables)
    DataType fieldName;

    // Methods (Actions)
    ReturnType methodName() {
        // Method body
    }
}

Class Members

Class members are elements defined within a class. These include:

  1. Fields (Instance Variables): Represent the state or attributes of an object.

  2. Methods: Define the behavior or actions an object can perform.

  3. Access Modifiers: Control the visibility and accessibility of fields and methods.

1. Fields (Instance Variables)

Fields are variables that store the state or data of an object.

Example:

String name;
int age;

2. Methods

Methods perform actions and define the behavior of a class. Methods can return values or perform tasks.

Example:

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

3. Access Modifiers

Access modifiers control the visibility of class members. The key access modifiers are:

  • public: The member can be accessed from any other class.

  • private: The member can only be accessed within the class itself.

  • protected: The member can be accessed within the package and by subclasses.

  • Default (Package-Private): When no modifier is specified, the member is accessible within the same package.

Example:

public String name;
private int age;
String address; // Default access (package-private)

Creating Instances of a Class

An object is an instance of a class. To create an object, use the new keyword, which allocates memory and initializes the object.

Syntax for Creating an Object

ClassName objectName = new ClassName();

Code Example with Access Modifiers and File Conventions

In Java, the file name must match the public class name. Other classes can reside in the same file, but only one class can be declared public.

Example Code:

Create a file named Main.java with the following content:

// The public class name matches the file name (Main.java)
public class Main {
    public static void main(String[] args) {
        // Creating instances of the Person class
        Person person1 = new Person();
        person1.name = "Alice";
        person1.age = 25;
        person1.introduce();

        Person person2 = new Person();
        person2.name = "Bob";
        person2.age = 30;
        person2.introduce();
    }
}

// Another class in the same file (non-public)
class Person {
    // Fields with different access modifiers
    public String name;      // Accessible everywhere
    int age;                 // Default access (package-private)

    // Method to introduce the person
    public void introduce() {
        System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
    }
}

Explanation of the Code

  1. File Name:
    The file is named Main.java because the public class is Main.

  2. Class Definitions:

    • The Main class contains the main method, which is the entry point of the program.

    • The Person class is defined in the same file but is not declared public, so it is accessible only within the same package.

  3. Access Modifiers:

    • The name field in the Person class is public, allowing it to be accessed directly from the Main class.

    • The age field has default (package-private) access, meaning it can be accessed from any class within the same package.

  4. Creating Objects:

    • Instances of the Person class are created using the new keyword.

    • Fields are assigned values, and the introduce() method is called to display the person's information.

Output

When you compile and run the program, you will get:

Hi, I'm Alice and I'm 25 years old.
Hi, I'm Bob and I'm 30 years old.

Key Points to Remember

  1. Class Structure: Classes define fields (state) and methods (behavior).

  2. Access Modifiers:

    • public, private, protected, and default (package-private).
  3. File Naming:

    • The public class name must match the file name.

    • Multiple non-public classes can exist in the same file.

  4. Object Creation:

    • Use the new keyword to instantiate objects.
  5. Encapsulation:

    • Proper use of access modifiers helps in encapsulating data and maintaining code integrity.

In Java, the static keyword provides a way to create class-level members that are shared across all instances of a class. This is useful when you need to maintain shared data or behavior, such as a counter that tracks the number of instances created. You can access static members without creating an instance of the class, which makes them efficient for tasks that don’t rely on object-specific data.

The final keyword is used to enforce immutability and prevent changes. Whether you want to declare constants, prevent method overriding, or avoid subclassing, final helps maintain integrity in your code.

Together, static and final play crucial roles in writing efficient and maintainable Java programs.

1. Static Variables and Methods

Static Variables

A static variable (also known as a class variable) is shared among all instances of a class. When a static variable is modified by one instance, the change is reflected across all other instances.

Key Points:

  • Declared using the static keyword.

  • Belongs to the class itself, not to any specific instance.

  • Initialized only once, at the time of class loading.

Static Methods

A static method belongs to the class rather than any instance. You can call a static method without creating an instance of the class.

Key Points:

  • Declared using the static keyword.

  • Can access static variables directly.

  • Cannot access instance variables or methods unless an instance is explicitly referenced.

Example of Static Variables and Methods

class Counter {
    // Static variable shared by all instances
    static int count = 0;

    // Static method to access the static variable
    static void displayCount() {
        System.out.println("Count: " + count);
    }

    // Instance method to increment the static variable
    void increment() {
        count++;
    }
}

public class Main {
    public static void main(String[] args) {
        Counter obj1 = new Counter();
        Counter obj2 = new Counter();

        obj1.increment();
        obj2.increment();

        // Accessing static method without creating an instance
        Counter.displayCount(); // Output: Count: 2
    }
}

Explanation

  1. Static Variable:
    count is a static variable that keeps track of the number of increments.

  2. Static Method:
    displayCount() can be called without creating an instance of Counter.

  3. Shared State:
    Both obj1 and obj2 modify the same count variable.

2. Static Blocks

A static block is used for static initialization. It runs once when the class is loaded into memory, before any objects are created.

Key Points:

  • Declared using static { }.

  • Used to initialize static variables or perform setup tasks.

Example of Static Blocks

class Database {
    static String dbName;

    // Static block for initialization
    static {
        dbName = "CustomerDB";
        System.out.println("Static block executed: Database initialized.");
    }

    static void connect() {
        System.out.println("Connecting to " + dbName);
    }
}

public class Main {
    public static void main(String[] args) {
        Database.connect();
    }
}

Output

Static block executed: Database initialized.
Connecting to CustomerDB

Explanation

  1. Static Block:
    The static block initializes the dbName variable when the class Database is first loaded.

  2. Single Execution:
    The static block runs only once, no matter how many objects are created.

3. The final Keyword

The final keyword is used to impose restrictions on variables, methods, and classes.

Final with Variables

  • When applied to variables, final makes them constants.

  • Once initialized, a final variable cannot be changed.

Example:

class Constants {
    final double PI = 3.14159;

    void display() {
        System.out.println("Value of PI: " + PI);
    }
}

public class Main {
    public static void main(String[] args) {
        Constants constants = new Constants();
        constants.display();
    }
}

Final with Methods

  • When applied to methods, final prevents them from being overridden in subclasses.

Example:

class Parent {
    final void show() {
        System.out.println("This is a final method.");
    }
}

class Child extends Parent {
    // Error: Cannot override the final method from Parent
    // void show() {
    //     System.out.println("Trying to override.");
    // }
}

Final with Classes

  • When applied to classes, final prevents them from being subclassed.

Example:

final class Vehicle {
    void display() {
        System.out.println("This is a vehicle.");
    }
}

// Error: Cannot subclass a final class
// class Car extends Vehicle {
// }

Comprehensive Example: Combining Static and Final

class Counter {
    // Static variable
    static int count = 0;

    // Final static constant
    static final int MAX_COUNT = 5;

    // Static block for initialization
    static {
        System.out.println("Static block executed: Counter class loaded.");
    }

    // Method to increment count with a check
    void increment() {
        if (count < MAX_COUNT) {
            count++;
            System.out.println("Count incremented to: " + count);
        } else {
            System.out.println("Maximum count reached: " + MAX_COUNT);
        }
    }

    // Static method to display count
    static void displayCount() {
        System.out.println("Current count: " + count);
    }
}

public class Main {
    public static void main(String[] args) {
        Counter obj1 = new Counter();
        Counter obj2 = new Counter();

        obj1.increment();
        obj2.increment();
        obj1.increment();
        obj2.increment();
        obj1.increment();
        obj2.increment(); // Should reach MAX_COUNT

        Counter.displayCount();
    }
}

Output

Static block executed: Counter class loaded.
Count incremented to: 1
Count incremented to: 2
Count incremented to: 3
Count incremented to: 4
Count incremented to: 5
Maximum count reached: 5
Current count: 5

Explanation

  1. Static Variable:
    count is shared among all instances.

  2. Static Constant:
    MAX_COUNT is a final static constant, representing the maximum allowable count.

  3. Static Block:
    The static block initializes when the Counter class is first loaded.

  4. Increment Logic:
    The increment() method checks if count has reached MAX_COUNT.

Key Takeaways

  1. Static Members:
    Shared across all instances and can be accessed without creating an object.

  2. Static Block:
    Runs once when the class is loaded, useful for static initialization.

  3. Final Keyword:

    • Variables: For constants (immutable values).

    • Methods: Prevents overriding.

    • Classes: Prevents subclassing.

When you create an object in Java, it needs to be initialized with meaningful values. Imagine creating a Car object—each car should have its own model and manufacturing year. Rather than setting these properties manually for every object, Java provides a mechanism called a constructor to initialize objects at the time of creation.

Constructors help ensure that every object starts in a valid state, either with default values or values provided by the user. This reduces the risk of uninitialized fields and improves code readability and maintainability.

Definition of Constructors

A constructor is a special method used to initialize objects. It has the same name as the class and does not have a return type—not even void. When an object is created, the constructor is called automatically to set up the initial state of the object.

Key Characteristics of Constructors:

  1. The name of the constructor matches the class name.

  2. It has no return type.

  3. It is called automatically when an object is created using the new keyword.

  4. You can have multiple constructors in a class (constructor overloading).

Types of Constructors

1. No-Argument Constructor

A no-argument constructor (also called a default constructor) initializes an object with default values. If no constructor is explicitly defined, Java provides a default constructor.

Syntax:

class ClassName {
    // No-argument constructor
    ClassName() {
        // Initialization code
    }
}

Example:

class Car {
    String model;
    int year;

    // No-argument constructor
    Car() {
        model = "Unknown";
        year = 2000;
    }

    void displayInfo() {
        System.out.println(model + " - " + year);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.displayInfo(); // Output: Unknown - 2000
    }
}

Explanation:

  • The no-argument constructor initializes model to "Unknown" and year to 2000.

  • When new Car() is called, this constructor is invoked.

2. Parameterized Constructor

A parameterized constructor allows you to initialize an object with user-specified values. This provides flexibility when creating objects with different initial states.

Syntax:

class ClassName {
    // Parameterized constructor
    ClassName(DataType param1, DataType param2) {
        // Initialization code
    }
}

Example:

class Car {
    String model;
    int year;

    // Parameterized constructor
    Car(String model, int year) {
        this.model = model;
        this.year = year;
    }

    void displayInfo() {
        System.out.println(model + " - " + year);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car2 = new Car("Toyota", 2021);
        car2.displayInfo(); // Output: Toyota - 2021
    }
}

Explanation:

  • The parameterized constructor takes model and year as parameters and initializes the fields accordingly.

  • The this keyword refers to the current object’s fields, differentiating them from the constructor parameters.

Constructor Overloading

Constructor overloading allows a class to have multiple constructors with different parameter lists. This provides multiple ways to initialize an object.

Example:

class Car {
    String model;
    int year;

    // No-argument constructor
    Car() {
        model = "Unknown";
        year = 2000;
    }

    // Parameterized constructor
    Car(String model, int year) {
        this.model = model;
        this.year = year;
    }

    // Another parameterized constructor with one parameter
    Car(String model) {
        this.model = model;
        this.year = 2022; // Default year
    }

    void displayInfo() {
        System.out.println(model + " - " + year);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car("Toyota", 2021);
        Car car3 = new Car("Honda");

        car1.displayInfo(); // Output: Unknown - 2000
        car2.displayInfo(); // Output: Toyota - 2021
        car3.displayInfo(); // Output: Honda - 2022
    }
}

Explanation:

  1. No-Argument Constructor: Initializes the fields with default values.

  2. Parameterized Constructor (Two Parameters): Allows specifying both model and year.

  3. Parameterized Constructor (One Parameter): Initializes model and sets year to a default value (2022).

When creating an object, the appropriate constructor is called based on the arguments passed.

Key Points to Remember

  1. Constructors are special methods used to initialize objects.

  2. A no-argument constructor provides default initialization.

  3. A parameterized constructor allows custom initialization with user-specified values.

  4. Constructor overloading enables multiple ways to initialize objects within a class.

  5. The this Keyword refers to the current object's fields and differentiates them from parameters.

Benefits of Using Constructors

  1. Ensures Initialization: Objects are always initialized properly.

  2. Encapsulation: Keeps object setup logic inside the class.

  3. Flexibility: Constructor overloading allows different initialization scenarios.

  4. Readability: Easier to understand how objects are set up.

Imagine you’re writing a Calculator class that needs to perform addition. Sometimes, you want to add two integers; other times, you might want to add two floating-point numbers. Instead of creating multiple methods with different names like addIntegers and addDoubles, wouldn't it be cleaner to have a single method named add that works with different types of arguments?

Method overloading allows you to define multiple methods with the same name but different parameters. This makes your code more readable, intuitive, and flexible by allowing methods to handle a variety of input types and numbers of arguments.

What is Method Overloading?

Method overloading occurs when multiple methods in the same class have the same name but different parameter lists. The compiler determines which method to call based on the number, type, and order of arguments provided during the method call.

Key Rules for Method Overloading

  1. Same Method Name: The methods must have the same name.

  2. Different Parameters: The methods must differ in:

    • The number of parameters, or

    • The type of parameters, or

    • The order of parameters (if types are different).

  3. Return Type: The return type can differ, but it does not play a role in determining which method to call.

Examples of Method Overloading

Example 1: Different Parameter Types

class Calculator {
    // Method to add two integers
    int add(int a, int b) {
        return a + b;
    }

    // Method to add two doubles
    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();

        System.out.println("Integer Addition: " + calc.add(5, 3));
        System.out.println("Double Addition: " + calc.add(5.5, 3.2));
    }
}

Output

Integer Addition: 8
Double Addition: 8.7

Explanation

  • The add(int a, int b) method handles integer addition.

  • The add(double a, double b) method handles double addition.

  • When calling calc.add(5, 3), the compiler selects the method with integer parameters.

  • When calling calc.add(5.5, 3.2), the compiler selects the method with double parameters.

Example 2: Different Number of Parameters

class Printer {
    // Method to print one message
    void print(String message) {
        System.out.println(message);
    }

    // Overloaded method to print a message multiple times
    void print(String message, int times) {
        for (int i = 0; i < times; i++) {
            System.out.println(message);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Printer printer = new Printer();

        printer.print("Hello, World!");
        printer.print("Java is fun!", 3);
    }
}

Output

Hello, World!
Java is fun!
Java is fun!
Java is fun!

Explanation

  • The print(String message) method prints the message once.

  • The print(String message, int times) method prints the message a specified number of times.

  • When calling printer.print("Hello, World!"), the compiler selects the single-parameter method.

  • When calling printer.print("Java is fun!", 3), the compiler selects the two-parameter method.

Example 3: Different Parameter Order

class Display {
    void show(String message, int count) {
        System.out.println("Message: " + message + ", Count: " + count);
    }

    void show(int count, String message) {
        System.out.println("Count: " + count + ", Message: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Display display = new Display();

        display.show("Overloading", 2);
        display.show(5, "Method");
    }
}

Output

Message: Overloading, Count: 2
Count: 5, Message: Method

Explanation

  • The methods show(String message, int count) and show(int count, String message) have the same name but different parameter orders.

  • The compiler resolves the correct method based on the argument order in the method call.

Benefits of Method Overloading

  1. Improves Code Readability:
    You can use the same method name for similar operations, making the code more intuitive.

  2. Flexibility:
    A single method name can handle different data types or numbers of arguments.

  3. Avoids Redundancy:
    No need to create multiple methods with different names for similar operations.

  4. Enhances Maintainability:
    Changes to the method's behavior can be localized to one method name, reducing the risk of errors.

Key Takeaways

  1. Method Overloading allows defining multiple methods with the same name but different parameters.

  2. The compiler determines which method to call based on the argument types, number, and order.

  3. Return types do not distinguish overloaded methods.

  4. Overloading enhances code readability, flexibility, and maintainability.

14
Subscribe to my newsletter

Read articles from Jyotiprakash Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jyotiprakash Mishra
Jyotiprakash Mishra

I am Jyotiprakash, a deeply driven computer systems engineer, software developer, teacher, and philosopher. With a decade of professional experience, I have contributed to various cutting-edge software products in network security, mobile apps, and healthcare software at renowned companies like Oracle, Yahoo, and Epic. My academic journey has taken me to prestigious institutions such as the University of Wisconsin-Madison and BITS Pilani in India, where I consistently ranked among the top of my class. At my core, I am a computer enthusiast with a profound interest in understanding the intricacies of computer programming. My skills are not limited to application programming in Java; I have also delved deeply into computer hardware, learning about various architectures, low-level assembly programming, Linux kernel implementation, and writing device drivers. The contributions of Linus Torvalds, Ken Thompson, and Dennis Ritchie—who revolutionized the computer industry—inspire me. I believe that real contributions to computer science are made by mastering all levels of abstraction and understanding systems inside out. In addition to my professional pursuits, I am passionate about teaching and sharing knowledge. I have spent two years as a teaching assistant at UW Madison, where I taught complex concepts in operating systems, computer graphics, and data structures to both graduate and undergraduate students. Currently, I am an assistant professor at KIIT, Bhubaneswar, where I continue to teach computer science to undergraduate and graduate students. I am also working on writing a few free books on systems programming, as I believe in freely sharing knowledge to empower others.