SynfraCore
Synfracore
Start Learning
Navigation

Academies

Platform

RoadmapsLabsCertificationsInterviewPYQsAI AssistantCareer
Start Learning Free🗺️ Learning Roadmaps

Java ProgrammingIntermediate

Applied knowledge and worked examples

✍️
Written by senior engineers. Reviewed for technical accuracy.· Updated 2025 · SynfraCore Java Programming Team
Expert Content

Java — Intermediate

Inheritance

java
// Base class (parent / superclass)
class Animal {
    String name;
    int age;

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

    void eat() {
        System.out.println(name + " is eating");
    }

    void makeSound() {
        System.out.println("Some generic sound");
    }
}

// Derived class (child / subclass)
class Dog extends Animal {
    String breed;

    Dog(String name, int age, String breed) {
        super(name, age);   // call parent constructor FIRST
        this.breed = breed;
    }

    @Override
    void makeSound() {      // method overriding
        System.out.println(name + " says: Woof!");
    }

    void fetch() {
        System.out.println(name + " fetches the ball");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog("Bruno", 3, "Labrador");
        d.eat();        // inherited from Animal
        d.makeSound();  // overridden — prints "Bruno says: Woof!"
        d.fetch();      // Dog-specific
    }
}

Polymorphism

Runtime Polymorphism (Dynamic Method Dispatch):

java
class Shape {
    void draw() { System.out.println("Drawing a shape"); }
}

class Circle extends Shape {
    @Override
    void draw() { System.out.println("Drawing a circle"); }
}

class Rectangle extends Shape {
    @Override
    void draw() { System.out.println("Drawing a rectangle"); }
}

public class Main {
    public static void main(String[] args) {
        // Parent reference pointing to child object
        Shape s1 = new Circle();        // valid: IS-A relationship
        Shape s2 = new Rectangle();

        s1.draw();   // "Drawing a circle"    — determined at RUNTIME
        s2.draw();   // "Drawing a rectangle" — this is polymorphism

        // Array of shapes
        Shape[] shapes = { new Circle(), new Rectangle(), new Circle() };
        for (Shape s : shapes) {
            s.draw();   // each calls its own version
        }
    }
}

Super-Sub Relationship rules (from notes):

Base class object reference can hold derived class object: Shape s = new Circle()
Derived class reference CANNOT hold base class object: Circle c = new Shape()
To access derived class specific methods via parent reference — use instanceof + casting

Abstract Classes and Interfaces

java
// Abstract class — cannot be instantiated, can have abstract + concrete methods
abstract class Vehicle {
    String brand;

    Vehicle(String brand) { this.brand = brand; }

    // Abstract method — no body, MUST be implemented by subclass
    abstract void start();

    // Concrete method — has body
    void stop() {
        System.out.println(brand + " stopped");
    }
}

class Car extends Vehicle {
    Car(String brand) { super(brand); }

    @Override
    void start() {
        System.out.println(brand + " car started with ignition key");
    }
}

// Interface — pure abstraction (Java 8+: can have default methods)
interface Flyable {
    void fly();   // implicitly public abstract

    default void land() {    // Java 8+ default method
        System.out.println("Landing safely");
    }
}

interface Swimmable {
    void swim();
}

// A class can implement multiple interfaces (not possible with abstract class)
class Duck extends Animal implements Flyable, Swimmable {
    @Override public void fly()  { System.out.println("Duck flying"); }
    @Override public void swim() { System.out.println("Duck swimming"); }
}

Abstract class vs Interface:

Use abstract class when classes share code (common base implementation)
Use interface when unrelated classes need to share a contract (can implement multiple)

Exception Handling

From the original Microsoft SDE notes — Exception Handling is critical for building robust applications.

Error Types in Java Development

1.Compile time errors — syntax errors, detected at compile time, fixed by programmer
2.Logical errors — wrong output due to misinterpretation of logic, detected at runtime
3.Runtime errors — invalid input or unexpected conditions at runtime

Exception Hierarchy

Throwable
├── Error (JVM level — don't catch these)
│   ├── StackOverflowError
│   ├── OutOfMemoryError
│   └── ...
└── Exception
    ├── Checked Exceptions (must handle or declare)
    │   ├── FileNotFoundException
    │   ├── ClassNotFoundException
    │   ├── InterruptedException
    │   └── IOException
    └── Unchecked Exceptions (RuntimeException)
        ├── ArithmeticException    (division by zero)
        ├── NullPointerException
        ├── ArrayIndexOutOfBoundsException
        ├── NumberFormatException
        └── ClassCastException

try-catch-finally

java
public class Division {
    public static void main(String[] args) {
        try {
            // Code that might throw exception
            String s1 = args[0];       // get dividend from command line
            String s2 = args[1];       // get divisor
            int a = Integer.parseInt(s1);
            int b = Integer.parseInt(s2);
            int result = a / b;
            System.out.println("Result = " + result);

        } catch (ArithmeticException ae) {
            System.out.println("Don't enter zero as divisor");
        } catch (NumberFormatException nfe) {
            System.out.println("Don't enter alphanumeric values");
        } catch (ArrayIndexOutOfBoundsException ab) {
            System.out.println("Please enter two numbers");
        } finally {
            // Always executes — use to close files, DB connections, etc.
            System.out.println("Program execution complete");
        }
    }
}

finally block rules (from notes):

1.Use finally to write cleanup code — release files, databases, network connections
2.finally executes compulsorily — whether or not exception occurs
3.Writing finally is optional but highly recommended for every Java program
4.Case 1: If exception occurs → part of try runs → catch block runs → finally runs
5.Case 2: No exception → complete try block runs → finally runs

Custom Exceptions

java
// Step 1: Create custom exception classes
class NegativeSalaryException extends Exception {
    NegativeSalaryException(String msg) {
        super(msg);
    }
}

class PsalException extends Exception {
    PsalException(String msg) {
        super(msg);
    }
}

// Step 2: Use them in your business logic
class Employee {
    void checkSalary(int salary) throws NegativeSalaryException, PsalException {
        if (salary < 0) {
            throw new NegativeSalaryException("Salary cannot be negative");
        }
        if (salary > 10000000) {
            throw new PsalException("Salary exceeds maximum limit");
        }
        System.out.println("Valid salary: " + salary);
    }
}

// Step 3: Handle them
public class SalaryChecker {
    public static void main(String[] args) {
        Employee emp = new Employee();
        try {
            emp.checkSalary(-5000);
        } catch (NegativeSalaryException nse) {
            System.out.println("Error: " + nse.getMessage());
        } catch (PsalException pe) {
            System.out.println("Error: " + pe.getMessage());
        }
    }
}

Multithreading

From notes — multithreading allows multiple threads to execute simultaneously.

Thread Lifecycle (State Chart Diagram)

New State → Ready State → Running State → Halted State
                ↑               ↓
                ←── Waiting State ──←
1.New State — Thread object created, memory not yet allocated
2.Ready State — Thread enters main memory, waiting for CPU
3.Running State — Thread currently executing under CPU control
4.Waiting State — Thread temporarily paused (sleep, wait, I/O)
5.Halted State — Thread completed execution

CPU Burst Time: The amount of time required by the thread from CPU for complete execution of user defined methods.

Creating Threads

java
// Method 1: Extend Thread class
class MyThread extends Thread {
    private String threadName;

    MyThread(String name) { this.threadName = name; }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(threadName + " — count: " + i);
            try {
                Thread.sleep(1000);   // pause 1 second
            } catch (InterruptedException ie) {
                System.out.println("Thread interrupted");
            }
        }
    }
}

// Method 2: Implement Runnable interface (PREFERRED — allows extending another class)
class PrintTask implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Value of i = " + i);
            try { Thread.sleep(1000); } catch (InterruptedException ie) {}
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // Method 1
        MyThread t1 = new MyThread("Thread-A");
        MyThread t2 = new MyThread("Thread-B");

        System.out.println("t1 alive before start: " + t1.isAlive());
        t1.start();   // DO NOT call run() directly — it won't create new thread
        t2.start();
        System.out.println("t1 alive after start: " + t1.isAlive());

        // Method 2
        Runnable task = new PrintTask();
        Thread t3 = new Thread(task);
        t3.start();
    }
}

Critical note from notes: It is NOT recommended to call run() directly. Call start() — it creates a new thread and then calls run() internally. Calling run() directly just executes it in the current thread without any multithreading benefit.

Synchronization

Without synchronization, multiple threads accessing shared data cause race conditions:

java
// Problem — without synchronization (inconsistent results)
class Account {
    private int balance = 0;

    // synchronized keyword ensures only one thread executes at a time
    synchronized void deposit(int amount) {
        balance = balance + amount;
        System.out.println("Current balance = " + balance);
    }

    int getBalance() { return balance; }
}

class Customer extends Thread {
    Account account;

    Customer(Account ac) { this.account = ac; }

    @Override
    public void run() {
        account.deposit(10);   // each customer deposits Rs.10
    }
}

public class BankDemo {
    public static void main(String[] args) throws InterruptedException {
        Account acc = new Account();

        // Create n customers (threads) all depositing into same account
        Customer c1 = new Customer(acc);
        Customer c2 = new Customer(acc);
        Customer c3 = new Customer(acc);

        c1.start(); c2.start(); c3.start();

        // Wait for all threads to complete
        c1.join(); c2.join(); c3.join();

        System.out.println("Final balance = " + acc.getBalance());  // 30 (correct)
    }
}

Collections Framework

java
import java.util.*;

// ArrayList — dynamic array
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.remove("Bob");
System.out.println(names.size());         // 2
System.out.println(names.get(0));         // Alice
System.out.println(names.contains("Bob")); // false

// LinkedList — doubly linked list
LinkedList<Integer> ll = new LinkedList<>();
ll.add(10);          // add to end
ll.addFirst(5);      // add to front
ll.addLast(20);      // add to back
System.out.println(ll.getFirst());  // 5
System.out.println(ll.getLast());   // 20
ll.removeFirst();

// LinkedList constructors:
// LinkedList() — creates empty list
// LinkedList(int size) — creates list with initial capacity

// HashMap — key-value pairs (no order)
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
System.out.println(scores.get("Alice"));   // 95
System.out.println(scores.containsKey("Bob")); // true
scores.remove("Bob");

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}

// HashSet — unique elements, no order
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("python");
tags.add("java");  // duplicate — ignored
System.out.println(tags.size());  // 2

// Stack — LIFO
Stack<Integer> stack = new Stack<>();
stack.push(1); stack.push(2); stack.push(3);
System.out.println(stack.pop());   // 3 (last in, first out)
System.out.println(stack.peek());  // 2 (view without removing)

Generics

Before generics (1D collection flow variable): internally JVM treated all entries as java.lang.Object. This caused issues — you could add any type, and getting it back required manual casting.

java
// Without generics (old way) — unsafe
List list = new ArrayList();
list.add("hello");
list.add(42);    // no error at compile time
String s = (String) list.get(1);  // ClassCastException at runtime!

// With generics (modern) — type-safe
List<String> strings = new ArrayList<>();
strings.add("hello");
// strings.add(42);  // compile error — caught early!
String s = strings.get(0);  // no cast needed

// Generic method
public static <T extends Comparable<T>> T findMax(T[] arr) {
    T max = arr[0];
    for (T item : arr) {
        if (item.compareTo(max) > 0) max = item;
    }
    return max;
}

Object Type Casting:

Converting base class object reference into derived class object reference
Syntax: SubclassName subObj = (SubclassName) baseClassObj;
Dynamic Object Type Casting: converting java.lang.Object reference into required derived class reference dynamically

Next Steps

Move to Advanced for: IO Streams, Network Programming (java.net), JDBC, Design Patterns, JVM internals, and Java 8+ features (Lambda, Stream API).

Share:
Join our Community
Exam tips, study groups, PYQ discussions — join learners preparing together
Up Next
🚀
Java ProgrammingAdvanced
Expert-level topics and analysis
Also Worth Exploring
← Back to all Java Programming modules
FundamentalsAdvanced