Week 2 - Methods & Parameters

Class 4 - Recording

▶ Watch Video

Methods & Parameters in Java

A method in Java is a reusable block of code that performs a specific task. Methods help you organize logic, reduce duplication, and make your programs easier to read and maintain.

1. What Is a Method?

General syntax of a method:

[access_modifier] [other_modifiers] return_type methodName(parameter_list) throws ExceptionType {
    // method body
}

1.1 Basic Example

This method takes two integers and returns their sum. It can be called multiple times with different values.

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

// Example usage:
int result = add(5, 3);   // result = 8

2. Parameters and Arguments

2.1 Parameter vs Argument

  • Parameter – the variable in the method definition.
  • Argument – the actual value passed when calling the method.
public void greet(String name) {   // 'name' is a parameter
    System.out.println("Hello, " + name);
}

// Example usage:
greet("Tech Tiger");                // "Tech Tiger" is an argument
// Output: Hello, Tech Tiger

2.2 Java Is Pass-by-Value (Very Important Concept)

Java is always pass-by-value. That means the method receives a copy of the value. For objects, the value being copied is a reference to the object.

2.2.1 Primitive Example (cannot change original)

public static void changeValue(int x) {
    x = 99;  // changes only the local copy
}

public static void main(String[] args) {
    int a = 10;
    changeValue(a);
    System.out.println(a); // Output: 10 (still 10, not 99)
}

The variable a in main does not change because the method received a copy of its value.

2.2.2 Reference Example (can change object state)

class Person {
    String name;
}

public static void rename(Person p) {
    p.name = "New Name";   // changes the object referenced by p
}

public static void main(String[] args) {
    Person person = new Person();
    person.name = "Old Name";

    rename(person);
    System.out.println(person.name);  // Output: New Name
}

Here the reference value is copied into the method, but both references point to the same object, so changing p.name affects person.name.

2.3 Varargs (...)

Varargs allow you to pass 0 or more arguments of the same type to a method.

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

public static void main(String[] args) {
    System.out.println(sum());              // Output: 0  (no args)
    System.out.println(sum(1, 2, 3));       // Output: 6
    System.out.println(sum(10, 20, 30, 40));// Output: 100
}
  • Only one vararg parameter is allowed per method.
  • It must be the last parameter in the list.

2.4 final Parameters

Using final in parameters prevents reassigning the parameter variable inside the method. It does not make the object itself immutable.

public void process(final String input) {
    // input = "something else"; // ❌ Compile-time error
    System.out.println(input);
}

Return Types & Method Overloading

3. Return Types

Every non-void method must return a value that matches its declared return type.

public double divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Divider cannot be zero");
    }
    return (double) a / b;
}

public void printHello() {          // returns nothing
    System.out.println("Hello");
}

If your method has a return type (like int, double, etc.), you must return a value on every execution path.

4. Method Overloading

Overloading means having multiple methods with the same name but different parameter lists (different number of parameters, types, or their order).

public int multiply(int a, int b) {
    return a * b;
}

public double multiply(double a, double b) {
    return a * b;
}

public int multiply(int a, int b, int c) {
    return a * b * c;
}

public static void main(String[] args) {
    System.out.println(multiply(2, 3));        // Output: 6        (int version)
    System.out.println(multiply(2.5, 3.0));    // Output: 7.5      (double version)
    System.out.println(multiply(2, 3, 4));     // Output: 24       (three-arg version)
}

Java decides which overload to call at compile time, based on the argument types.

Exceptions and Methods

5. Exceptions and Methods

Methods can signal error conditions using exceptions. They can either handle exceptions internally (with try-catch) or declare that they throw them using the throws clause.

5.1 Declaring Exceptions with throws

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public void readFile(String path) throws IOException {
    Files.readAllLines(Paths.get(path));  // may throw IOException
}

public void doWork() {
    try {
        readFile("data.txt");
        System.out.println("File read successfully.");
    } catch (IOException e) {
        System.out.println("Error reading file: " + e.getMessage());
    }
}

5.2 Checked vs Unchecked Exceptions

  • Checked exceptions (subclasses of Exception except RuntimeException) must be handled or declared.
  • Unchecked exceptions (subclasses of RuntimeException) do not need to be declared.
public int parse(String s) {
    // NumberFormatException is an unchecked exception (RuntimeException)
    return Integer.parseInt(s);
}

public static void main(String[] args) {
    System.out.println(parse("123")); // OK
    System.out.println(parse("abc")); // Throws NumberFormatException at runtime
}

Core Method Concepts

6. Core Method Concepts

6.1 Static vs Instance Methods

Static methods belong to the class; instance methods belong to an object.

class MathUtil {

    // Static method (no instance required)
    public static int square(int x) {
        return x * x;
    }

    // Instance method (needs an object)
    public int triple(int x) {
        return 3 * x;
    }
}

public static void main(String[] args) {
    // Call static method
    System.out.println(MathUtil.square(4)); // Output: 16

    // Call instance method
    MathUtil util = new MathUtil();
    System.out.println(util.triple(4));     // Output: 12
}

6.2 Scope and the Call Stack

Variables declared inside a method are local to that method and exist only while the method runs.

public void demo() {
    int x = 10;            // local variable
    System.out.println(x); // Output: 10
} // x is destroyed after this method finishes

Each method call creates a new stack frame. Deep or infinite recursion can cause StackOverflowError.

6.3 Recursion Example

public static int factorial(int n) {
    if (n < 0) {
        throw new IllegalArgumentException("n must be >= 0");
    }
    if (n == 0 || n == 1) {
        return 1;               // base case
    }
    return n * factorial(n - 1);// recursive call
}

public static void main(String[] args) {
    System.out.println(factorial(5)); // Output: 120
}

Advanced Method Topics

7. Interesting & Lesser-Known Topics

7.1 Covariant Return Types

When overriding a method, the subclass can return a more specific type (a subtype of the original return type).

class Animal {}

class Dog extends Animal {}

class AnimalFactory {
    Animal create() {
        return new Animal();
    }
}

class DogFactory extends AnimalFactory {
    @Override
    Dog create() {          // covariant return type (Dog is a subclass of Animal)
        return new Dog();
    }
}

This allows code that uses DogFactory to directly get a Dog without casting.

7.2 Method References & Lambdas

Methods can be treated like values using method references, often used with streams and collections.

import java.util.Arrays;
import java.util.List;

public class MethodRefDemo {

    public static void printUpper(String s) {
        System.out.println(s.toUpperCase());
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("java", "python", "kotlin");

        // Using lambda
        names.forEach(s -> MethodRefDemo.printUpper(s));

        // Using method reference
        names.forEach(MethodRefDemo::printUpper);
        // Output:
        // JAVA
        // PYTHON
        // KOTLIN
    }
}

7.3 Default Methods in Interfaces

Interfaces can have default method implementations, which helps add new behavior without breaking existing classes.

interface Logger {
    default void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

class Service implements Logger {
    public void doWork() {
        log("Service started");
    }
}

public class Main {
    public static void main(String[] args) {
        Service s = new Service();
        s.doWork();  // Output: [LOG] Service started
    }
}

7.4 Overloading with Varargs (Ambiguity)

Overloading and varargs can create confusing situations. The compiler picks the most specific method.

public void print(int x) {
    System.out.println("int");
}

public void print(Integer... x) {
    System.out.println("Integer varargs");
}

public static void main(String[] args) {
    Example e = new Example();
    e.print(5);  // Output: int  (primitive int is more specific)
}

7.5 Shadowing and Hiding

A parameter or local variable can shadow a field with the same name.

class Example {
    int value = 10;

    public void show(int value) {
        System.out.println(value);      // parameter
        System.out.println(this.value); // field
    }
}

public static void main(String[] args) {
    Example e = new Example();
    e.show(5);
    // Output:
    // 5
    // 10
}

Static methods are hidden, not overridden:

class A {
    static void hello() {
        System.out.println("Hello from A");
    }
}

class B extends A {
    static void hello() {
        System.out.println("Hello from B");
    }
}

public static void main(String[] args) {
    A a = new B();
    a.hello(); // Output: Hello from A (decided by reference type A)
}

7.6 Reflection: Calling Methods Dynamically

Reflection lets you inspect and invoke methods at runtime, even if you didn't know them at compile time. This is used heavily in frameworks (e.g., Spring), but it can be slower and harder to debug.

import java.lang.reflect.Method;

public class ReflectionDemo {
    public void sayHi(String name) {
        System.out.println("Hi, " + name);
    }

    public static void main(String[] args) throws Exception {
        ReflectionDemo obj = new ReflectionDemo();

        Class<?> clazz = obj.getClass();
        Method m = clazz.getMethod("sayHi", String.class);

        m.invoke(obj, "Tech Tiger"); // Output: Hi, Tech Tiger
    }
}

Complete Example & Best Practices

8. Complete Example: Putting It All Together

This example combines several ideas: static methods, instance methods, parameters, return values, exceptions, and varargs.

public class MethodDocDemo {

    // Static utility method with exception
    public static double safeDivide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Divider cannot be zero");
        }
        return (double) a / b;
    }

    // Overloaded method with varargs
    public static int sum(int... values) {
        int total = 0;
        for (int v : values) {
            total += v;
        }
        return total;
    }

    // Instance method using recursion
    public int factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n must be >= 0");
        }
        if (n == 0 || n == 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        // Using static method
        System.out.println("10 / 2 = " + safeDivide(10, 2)); // Output: 10 / 2 = 5.0

        // Using varargs method
        System.out.println("Sum = " + sum(1, 2, 3, 4, 5));   // Output: Sum = 15

        // Using instance method
        MethodDocDemo demo = new MethodDocDemo();
        System.out.println("Factorial(5) = " + demo.factorial(5)); // Output: 120
    }
}

Best Practices for Methods

  • Use descriptive method names that clearly indicate what the method does
  • Keep methods focused on a single task (Single Responsibility Principle)
  • Avoid too many parameters - consider using objects or builder patterns
  • Handle exceptions appropriately - use checked exceptions for recoverable errors
  • Document complex methods with JavaDoc comments
  • Make methods as simple and readable as possible
  • Use static methods for utility functions that don't depend on instance state

Class 5 - Recording

▶ Watch Video

Arrays & String Manipulation in Java

1. Arrays in Java

Arrays are one of the fundamental data structures in Java. They allow you to store multiple values of the same data type under one variable name. Arrays are fast, memory-efficient, and ideal when the number of elements is known beforehand. However, arrays are also fixed in size, meaning once you create them, you cannot increase or decrease their length.

1.1 What is an Array?

An array is a sequence of values stored in contiguous memory locations. Each element can be accessed using an index, starting from 0. Arrays store elements of a single data type, such as integers, strings, or objects. Because of their predictable structure, arrays offer very fast access time—accessing any element is always O(1).

int[] numbers = new int[5]; // creates an array with 5 integer slots

1.2 Declaring and Initializing Arrays

Declaration

Declaring an array only tells Java to expect an array variable—it does not create memory yet. Declaring does not allocate space until you use the new keyword or assign a literal array.

int[] arr;
int arr2[]; // valid but less preferred

Allocation

Allocation creates memory for a specific number of elements. The number you provide becomes the array’s final size.

arr = new int[3]; // array of size 3

Declaration + Initialization

You can declare and populate an array in one step. This is common when you already know the values.

int[] nums = {10, 20, 30};
String[] names = new String[] {"Alice", "Bob", "Charlie"};

1.3 Accessing and Updating Elements

Elements are accessed by index. Trying to access an index that does not exist results in an exception. The length field provides the total number of array elements.

int[] nums = {10, 20, 30};
int first = nums[0];   // 10
nums[1] = 99;          // update second element
int length = nums.length; // 3

1.4 Iterating Over Arrays

Classic for-loop

The traditional for-loop provides access to both the value and the index, which is helpful when you need to modify specific elements or use their positions.

for (int i = 0; i < nums.length; i++) {
    System.out.println("Index " + i + ": " + nums[i]);
}

Enhanced for-loop

Also known as the “for-each loop,” this version focuses only on values, making it ideal for reading array elements without caring about indexes.

for (int value : nums) {
    System.out.println(value);
}

1.5 Multi-Dimensional Arrays

Java supports multi-dimensional arrays, which can be visualized as tables or grids. These arrays are actually arrays of arrays.

int[][] matrix = new int[2][3]; // 2 rows, 3 columns

Jagged Arrays

Unlike some languages, Java allows different row lengths in multi-dimensional arrays. Such arrays are called jagged arrays. They give more flexibility and save memory if rows differ in size.

int[][] jagged = new int[3][];
jagged[0] = new int[]{1, 2};
jagged[1] = new int[]{3, 4, 5};
jagged[2] = new int[]{6};

1.6 Arrays Utility Class

The java.util.Arrays class provides powerful functions that save time and reduce complexity. These include sorting, searching, copying, filling, comparing, and converting arrays to String.

Arrays.sort(nums); // ascending order
Arrays.copyOf(nums, 3); // copy first 3 elements
Arrays.fill(nums, 7); // fill array with value

1.7 Common Array Exceptions

ArrayIndexOutOfBoundsException

This occurs when you try to access an index outside valid boundaries (0 to length - 1). It is one of the most common beginner errors.

nums[3]; // invalid if array has only 3 elements

NullPointerException

Happens when the array reference itself is null and you try to access properties or elements.

int[] a = null;
a.length; // causes NPE

1.8 Less-Known Facts about Arrays

  • Arrays are full-fledged objects in Java—even int[] extends Object.
  • The length property of arrays is final, meaning array size cannot change once created.
  • Java arrays are covariant, meaning String[] is a subtype of Object[], but this can cause runtime errors.
  • Arrays of reference types store null by default.

2. Strings in Java

Strings are one of the most used data types in Java. A String represents a sequence of characters, and Java treats Strings as immutable objects. This immutability is a core design principle that ensures security, thread safety, and optimization.

2.1 What is a String?

A String stores characters using UTF-16 encoding. Each character is stored in one or two bytes, depending on its Unicode value. Java stores String literals in a special area called the "String Pool" to reduce memory usage and avoid duplicate objects.

2.2 Creating Strings

Strings can be created as literals or using the new keyword. Literal Strings are stored in a shared pool, while new String() always creates a fresh object on the heap.

String s1 = "Hello"; // uses String Pool
String s2 = new String("Hello"); // always new object

2.3 Basic String Operations

Concatenation

Combining multiple strings:

String c = "Hello" + " World";
String d = "Hello".concat(" World");

Length & Character Access

String str = "Java";
str.length(); // number of characters
str.charAt(1); // returns 'a'

Substring Extraction

Creates a new string from a portion of an existing one:

t.substring(0, 5);
t.substring(6);

Search Operations

Locate characters or sequences inside the string:

text.indexOf('a');
text.lastIndexOf("desh");

Replacing Text

"banana".replace("na","NA");

2.4 Comparing Strings

equals() vs ==

== compares object references, not content. equals() compares character sequences.

Case-insensitive comparison

a.equalsIgnoreCase(b);

2.5 Useful String Methods

  • trim() removes leading and trailing spaces.
  • toUpperCase() and toLowerCase()
  • startsWith() and endsWith()
  • contains() to check substring existence.
  • split() to convert text into array pieces.
  • isEmpty() to check if length == 0.

2.6 StringBuilder & StringBuffer

Since Strings are immutable, frequent modifications (like inside loops) create many unnecessary objects. StringBuilder and StringBuffer solve this by providing mutable character sequences.

StringBuilder (fast, not thread-safe)

StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" World");

StringBuffer (thread-safe)

StringBuffer sbuf = new StringBuffer();
sbuf.append("Safe Threads");

2.7 Strings & Character Arrays

Converting to a char array is useful for low-level manipulation, like reversing strings or masking passwords.

char[] chars = "Hello".toCharArray();
String rebuilt = new String(chars);

2.8 Common String Exceptions

StringIndexOutOfBoundsException

Occurs when accessing an invalid index:

"Java".charAt(4);

NullPointerException

String s = null;
s.length();

2.9 Less-Known String Facts

  • Strings are stored in the String Pool to save memory.
  • Strings are immutable, which increases security and reduces bugs.
  • Some characters require two char values due to UTF-16 surrogate pairs.
  • intern() allows manual control of the String Pool.

3. Arrays & Strings Together

3.1 Splitting Strings into Arrays

Useful for reading CSV data, parsing input, etc.

String[] fruits = "apple,banana,orange".split(",");

3.2 Joining Arrays into Strings

String.join(" ", new String[]{"Java","is","fun"});

3.3 Reversing a String using Arrays

char[] c = "hello".toCharArray();
// reverse logic
String reversed = new String(c);

4. Best Practices Summary

  • Use arrays when the size is fixed and performance is critical.
  • Use ArrayList when size changes dynamically.
  • Always use equals() (not ==) for string comparison.
  • Use StringBuilder for heavy string manipulation.
  • Always check for null before using string methods.
  • Prefer enhanced for-loop for readability.
  • Use Arrays.toString() for printing arrays.

Class 6 - Recording

▶ Watch Video

Class in Java

Definition

A class in Java is a blueprint/template used to create objects. It contains:

  • Variables (fields)
  • Methods (behaviors)
  • Constructors
  • Blocks
  • Inner classes

Simple Class Example


class Car {
    String brand;
    int speed;

    void run() {
        System.out.println(brand + " is running at " + speed + " km/h");
    }
}

Real World Explanation

The Car class is like a car blueprint. A blueprint is not a real car; it only describes what a car should have.

  • brand → like "Toyota", "BMW"
  • speed → how fast it can move
  • run() → behavior of the car

Types of Classes (All Types With Real-World Examples)

1. Concrete (Regular) Class


class Employee {
    String name;
    int id;
}

Real World: A form used to create real employees.

2. Abstract Class


abstract class Animal {
    abstract void sound();
}

Real World: All animals make sounds, but each type defines its own.

3. Final Class


final class Constants { }

Real World: Sealed class — cannot be extended or modified.

4. Static Nested Class


class Car {
    static class Engine {
        void info() { System.out.println("Engine info"); }
    }
}

Real World: Department inside a company that works independently.

5. Member Inner Class


class A {
    class B {
        void message() {
            System.out.println("Inner class B");
        }
    }
}

6. Local Inner Class


class Shop {
    void bill() {
        class Item {
            String name;
        }
    }
}

Real World: Temporary class used only while generating bill.

7. Anonymous Inner Class


Runnable r = new Runnable() {
    public void run() {
        System.out.println("Running task");
    }
};

Real World: A temporary worker hired for only one job.

8. Enum Class


enum Direction { NORTH, SOUTH, EAST, WEST }

9. POJO Class


class Student {
    private String name;
    private int age;
}

10. Immutable Class


final class User {
    private final String username;
    User(String username) { this.username = username; }
    public String getUsername() { return username; }
}

11. Singleton Class


class Singleton {
    private static Singleton obj = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return obj;
    }
}

Objects

Definition

An object is a real instance of a class containing:

  • State (variables)
  • Behavior (methods)
  • Identity (memory address)

Creating Objects (All Ways)

1. Using new keyword


Car c = new Car();

2. Using Reflection


Car c = Car.class.newInstance();

3. Using Constructor class


Constructor cons = Car.class.getConstructor();
Car c = cons.newInstance();

4. Using clone()


Car c2 = (Car)c1.clone();

5. Using Deserialization


Car c = (Car)ois.readObject();

6. Anonymous Object


new Car().run();

Memory Diagram

Stack: c → reference
Heap: Object { brand="BMW", speed=180 }
Method Area: Class Car loaded

Constructors in Java

A constructor in Java is a special block of code used to initialize an object. It runs automatically when an object is created and prepares the object with initial values. It has the same name as the class and no return type.

Why Do We Need Constructors?

Constructors allow your object to start its life with proper and meaningful initial values. Without constructors:

  • Objects would start uninitialized
  • We would do manual setup each time
  • Code becomes repetitive and error-prone


Rules of Constructors

  • Constructor name must be the same as class name
  • No return type (not even void)
  • Called automatically when object is created
  • Cannot be static, final, or abstract
  • Can be overloaded
  • super() must be the first statement in constructor
  • If no constructor is written, JVM creates a default one

Types of Constructors (All Types with Deep Explanation)

1. Default Constructor (JVM Provided)

If you do not write any constructor, Java automatically provides a default constructor that sets all fields to default values specific to data types.


class Car {
    int speed;      // default = 0
    String brand;   // default = null
    boolean on;     // default = false
}

Explanation

Here, Java silently adds:


Car() {
    super(); // call to Object class
}

This constructor does nothing except calling parent class constructor.

Real World Example

It's like building a car from the factory with: no custom features. Everything is set to its default state: speed = 0, engine OFF, no color set.


2. No-Argument (No-Arg) Constructor

This is a constructor that accepts no parameters and sets object values manually.


class Car {
    String brand;
    int speed;

    Car() {
        brand = "Unknown";
        speed = 0;
        System.out.println("No-arg constructor executed");
    }
}

Explanation

When you create:


Car c = new Car();

It sets initial values like:

  • brand = "Unknown"
  • speed = 0

Real World Example

You buy a car without selecting any features → Factory gives you a default model with standard features.


3. Parameterized Constructor

Used when you want to initialize objects with specific values.


class Car {
    String brand;
    int speed;

    Car(String brand, int speed) {
        this.brand = brand; // the brand variable of this object
        this.speed = speed;
    }
}

Explanation

Here:


Car c = new Car("BMW", 180);

It immediately creates a BMW car with 180 speed.

Real World Example

You place a custom order for a car:

  • You choose the brand
  • You choose the engine/speed
Factory manufactures exactly what you want.


4. Copy Constructor

A user-defined constructor that copies one object into another. Java DOES NOT provide it automatically.


class Car {
    String brand;
    int speed;

    Car(Car c) {        // copy constructor
        this.brand = c.brand;
        this.speed = c.speed;
    }
}

Explanation

You can write:


Car c1 = new Car("Tesla", 200);
Car c2 = new Car(c1); // copying object

Now c2 has the same values as c1.

Real World Example

You ask the factory to create “the same car as my friend’s car”. The factory clones the exact configuration.


5. Private Constructor

Used mostly for Singleton Pattern or factory classes. It prevents others from creating objects.


class DatabaseConnection {
    private DatabaseConnection() {
        System.out.println("Database connected");
    }

    private static DatabaseConnection instance = new DatabaseConnection();

    public static DatabaseConnection getInstance() {
        return instance;
    }
}

Real World Use Case

A Database should have only one connection. Private constructor ensures no one creates multiple objects.


6. Protected Constructor

Accessible within:

  • same package
  • child classes (even if in different packages)


class Vehicle {
    protected Vehicle() {
        System.out.println("Protected Vehicle Constructor");
    }
}

Real World Example

You can only build this type of vehicle if:

  • You are part of the same company (package)
  • Or you own a child company (subclass)


7. Constructor Overloading

You can create multiple constructors with different sets of parameters.


class Car {
    Car() { System.out.println("Default car"); }
    Car(int speed) { this.speed = speed; }
    Car(String brand, int speed) { this.brand = brand; this.speed = speed; }
}

Real World Example

Different ways to buy a car:

  • Buy base model
  • Select speed only
  • Select brand and speed


8. Constructor Chaining

Calling one constructor from another using this() or super().


class Car {
    String brand;
    int speed;

    Car() {
        this("Unknown", 0); // calling another constructor
    }

    Car(String brand, int speed) {
        this.brand = brand;
        this.speed = speed;
    }
}

Real World Example

A base car model always uses a more detailed manufacturing process internally.

Using super() in Inheritance


class Vehicle {
    Vehicle() {
        System.out.println("Vehicle Constructor");
    }
}

class Car extends Vehicle {
    Car() {
        super();
        System.out.println("Car Constructor");
    }
}

Output:


Vehicle Constructor
Car Constructor

Complete Real-World Constructor Example


class Car {

    String brand;
    int speed;
    String fuelType;

    // No-argument constructor
    Car() {
        this("Unknown", 0, "Petrol");
        System.out.println("Default car created.");
    }

    // Parameterized constructor
    Car(String brand, int speed, String fuelType) {
        this.brand = brand;
        this.speed = speed;
        this.fuelType = fuelType;
    }

    // Copy constructor
    Car(Car other) {
        this.brand = other.brand;
        this.speed = other.speed;
        this.fuelType = other.fuelType;
    }

    void show() {
        System.out.println("Brand: " + brand);
        System.out.println("Speed: " + speed);
        System.out.println("Fuel: " + fuelType);
    }
}

public class Main {
    public static void main(String[] args) {

        Car c1 = new Car();  // no-arg
        c1.show();

        Car c2 = new Car("Tesla", 200, "Electric"); // parameterized
        c2.show();

        Car c3 = new Car(c2); // copy constructor
        c3.show();
    }
}

This demonstrates all constructor types in a real-world scenario.


Conclusion

Constructors are the backbone of object initialization in Java. They:

  • Provide meaningful starting values
  • Reduce code duplication
  • Offer flexibility through overloading
  • Ensure safe object creation using private/protected constructors
  • Support inheritance with super()
Understanding constructors is essential to master Object-Oriented Programming.

Class 7 - Recording

▶ Watch Video

Inheritance in Java

Inheritance allows a subclass to acquire the properties and behaviors of a superclass. It promotes code reusability and method overriding.

Example:


class Animal {
    void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound(); // Output: Dog barks
    }
}

        

Important Points:

  • Java does not support multiple inheritance with classes.
  • All classes inherit from the Object class.
  • Use super to access parent methods or constructors.

Related Exceptions:


// ClassCastException example
Animal a = new Animal();
Dog d = (Dog) a; // Throws ClassCastException

        

Polymorphism in Java

Polymorphism means one interface, many implementations. It allows methods to behave differently based on the object.

1. Compile-time Polymorphism (Method Overloading)


class MathOps {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

        

2. Runtime Polymorphism (Method Overriding)


class Bird {
    void fly() {
        System.out.println("Bird is flying");
    }
}

class Eagle extends Bird {
    void fly() {
        System.out.println("Eagle flies high");
    }
}

public class Test {
    public static void main(String[] args) {
        Bird b = new Eagle();
        b.fly(); // Output: Eagle flies high
    }
}

        

Important Points:

  • Method overloading is resolved at compile-time.
  • Method overriding is resolved at runtime.
  • Overridden methods must have the same signature.
  • Return type can be covariant (subtype allowed).

Related Exceptions:


// Wrong downcasting -> ClassCastException
Bird b = new Bird();
Eagle e = (Eagle) b; // Throws ClassCastException

        

Encapsulation in Java

Encapsulation wraps data (variables) and methods within a class and protects them from unauthorized access using private access modifiers.

Example:


class Student {
    private String name;
    private int age;

    public String getName() { return name; }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() { return age; }

    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            System.out.println("Invalid age");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        s.setName("John");
        s.setAge(20);

        System.out.println(s.getName());
        System.out.println(s.getAge());
    }
}

        

Important Points:

  • Use private variables and public getters/setters.
  • Encapsulation protects data and enables validation.
  • Improves maintainability and flexibility.

Related Exceptions:


// Custom exception in setter
public void setAge(int age) {
    if (age <= 0) {
        throw new IllegalArgumentException("Age must be positive");
    }
    this.age = age;
}