Java Intermediate Course
Level up your Java with 30 intermediate lessons. Deep-dive into OOP, inheritance, polymorphism, interfaces, exceptions, generics, and collections — each with code, exact output, and official Oracle documentation links.
Start LearningOOP Review & Method Overloading
Method overloading means defining multiple methods with the same name but different parameter lists. Java picks the right one based on the number and type of arguments. Return type alone cannot distinguish overloaded methods.
public class Main {
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(5, 3));
System.out.println(add(5.5, 3.2));
}
}
Method Overriding & @Override
Overriding happens when a subclass redefines a method from its parent. The method signature stays identical, but the behavior changes. The @Override annotation tells the compiler to verify that you are truly overriding.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Animal();
a.sound();
Animal d = new Dog();
d.sound();
}
}
Inheritance & extends
Inheritance lets one class acquire the fields and methods of another. The extends keyword establishes the parent-child relationship. Java supports single inheritance — a class can extend only one parent.
class Vehicle {
String brand = "Generic";
void start() {
System.out.println("Vehicle started");
}
}
class Car extends Vehicle {
void honk() {
System.out.println("Beep beep");
}
}
public class Main {
public static void main(String[] args) {
Car c = new Car();
System.out.println(c.brand);
c.start();
c.honk();
}
}
super Keyword
super refers to the parent class. Use super() to call the parent constructor, and super.method() to call an overridden parent method. This lets a subclass extend rather than replace parent behavior.
class Person {
String name;
Person(String name) {
this.name = name;
}
void print() {
System.out.println("Name: " + name);
}
}
class Student extends Person {
int grade;
Student(String name, int grade) {
super(name);
this.grade = grade;
}
@Override
void print() {
super.print();
System.out.println("Grade: " + grade);
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("Ali", 10);
s.print();
}
}
Abstract Classes
An abstract class cannot be instantiated — it exists to be extended. It can have abstract methods (no body) that subclasses must implement, plus concrete methods with code. Abstract classes provide a common template.
abstract class Shape {
abstract double area();
void print() {
System.out.println("This is a shape");
}
}
class Circle extends Shape {
double radius;
Circle(double r) {
radius = r;
}
@Override
double area() {
return 3.14 * radius * radius;
}
}
public class Main {
public static void main(String[] args) {
Circle c = new Circle(2);
System.out.println(c.area());
c.print();
}
}
Interfaces
An interface is a contract that defines method signatures with no bodies. A class implements an interface and must provide all the methods. Interfaces enable polymorphism across unrelated class hierarchies.
interface Drawable {
void draw();
}
class Circle implements Drawable {
public void draw() {
System.out.println("Drawing a circle");
}
}
class Square implements Drawable {
public void draw() {
System.out.println("Drawing a square");
}
}
public class Main {
public static void main(String[] args) {
Drawable d1 = new Circle();
Drawable d2 = new Square();
d1.draw();
d2.draw();
}
}
Multiple Interfaces
A class can implement multiple interfaces, giving it multiple types. This is Java's answer to multiple inheritance — a class can inherit behavior from one parent but implement many contracts.
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
public void fly() {
System.out.println("Duck flies");
}
public void swim() {
System.out.println("Duck swims");
}
}
public class Main {
public static void main(String[] args) {
Duck d = new Duck();
d.fly();
d.swim();
}
}
Polymorphism
Polymorphism means 'many forms'. A parent-type reference can point to any child object, and method calls are resolved at runtime based on the actual object type. This lets one line of code work with many object types.
class Animal {
void speak() {
System.out.println("Some sound");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("Meow");
}
}
class Cow extends Animal {
@Override
void speak() {
System.out.println("Moo");
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = { new Cat(), new Cow(), new Animal() };
for (Animal a : animals) {
a.speak();
}
}
}
Encapsulation Deep Dive
Encapsulation bundles data (fields) with the methods that operate on them, and hides internal state using private access. Data is accessed only through controlled public methods, protecting integrity and reducing coupling.
class Account {
private double balance = 0;
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
double getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
Account a = new Account();
a.deposit(100);
a.withdraw(40);
a.withdraw(100);
System.out.println(a.getBalance());
}
}
Packages & Imports
Packages group related classes and prevent name conflicts. The package statement declares the package, and import brings classes from other packages into scope. The fully qualified name includes the package path.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Ali");
names.add("Sara");
System.out.println(names);
}
}
Exception Handling (try/catch)
Exceptions are runtime errors that disrupt normal flow. try/catch handles them gracefully: code in try is monitored, and catch handles specific exception types. Without handling, the program would crash.
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("Program continues");
}
}
finally & try-with-resources
finally always executes whether an exception occurred or not — ideal for cleanup. try-with-resources automatically closes resources like files and scanners, removing the need for manual close() calls.
public class Main {
public static void main(String[] args) {
try {
System.out.println("Inside try");
int x = 5 / 1;
System.out.println(x);
} finally {
System.out.println("Inside finally");
}
System.out.println("Done");
}
}
throw & throws
throw creates an exception inside a method. throws declares that a method may throw a checked exception, pushing the responsibility to the caller. Together they let you propagate errors up the call stack.
public class Main {
static void checkAge(int age) throws IllegalArgumentException {
if (age < 18) {
throw new IllegalArgumentException("Too young");
}
System.out.println("Age valid");
}
public static void main(String[] args) {
try {
checkAge(15);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
Custom Exceptions
You can create your own exception classes by extending Exception (checked) or RuntimeException (unchecked). Custom exceptions make errors meaningful and domain-specific.
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
public class Main {
static void withdraw(double balance, double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Not enough money");
}
System.out.println("Withdrawn: " + amount);
}
public static void main(String[] args) {
try {
withdraw(50, 100);
} catch (InsufficientFundsException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
Generics Basics
Generics let classes and methods work with any type while keeping type safety. A type parameter like <T> is replaced with a concrete type at compile time, preventing ClassCastException at runtime.
class Box<T> {
private T value;
void set(T v) {
value = v;
}
T get() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
}
}
Generic Classes
A generic class declares one or more type parameters. You can use multiple parameters, like <K, V> for key-value pairs. This is the basis of Java's collection framework.
class Pair<K, V> {
private K key;
private V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
K getKey() {
return key;
}
V getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> p = new Pair<>("Age", 25);
System.out.println(p.getKey() + ": " + p.getValue());
}
}
Generic Methods
Methods can have their own type parameters, independent of the class. The type parameter is declared before the return type, like <T> T method(T input). This makes a single method work with any type.
public class Main {
static <T> void printArray(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
public static void main(String[] args) {
String[] words = { "A", "B" };
Integer[] nums = { 1, 2 };
printArray(words);
printArray(nums);
}
}
Collections Framework Overview
The Collections Framework is a set of interfaces and classes for storing and processing groups of objects. The main interfaces are List (ordered), Set (no duplicates), and Map (key-value). ArrayList, HashSet, and HashMap are common implementations.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("A");
Set<String> set = new HashSet<>();
set.add("A");
set.add("A");
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
System.out.println("List: " + list);
System.out.println("Set: " + set);
System.out.println("Map: " + map);
}
}
List & ArrayList Deep Dive
ArrayList is a resizable array implementation of List. It provides indexed access, so get(index) is fast. Common methods include add, get, set, remove, size, and contains. It is the most-used List implementation.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> items = new ArrayList<>();
items.add("Apple");
items.add("Banana");
items.add("Mango");
items.remove(1);
items.set(1, "Orange");
System.out.println(items.size());
System.out.println(items.get(0));
System.out.println(items.contains("Apple"));
System.out.println(items);
}
}
LinkedList
LinkedList stores elements as nodes linked in both directions. Insertions and deletions in the middle are faster than ArrayList, but random access get(index) is slower. It also implements Deque, so it can act as a queue.
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<String> queue = new LinkedList<>();
queue.add("First");
queue.add("Second");
queue.add("Third");
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue);
}
}
Set & HashSet
Set is a collection with no duplicate elements. HashSet is the most common implementation, backed by a hash table. It offers O(1) average time for add, remove, and contains, and does not guarantee order.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> names = new HashSet<>();
names.add("Ali");
names.add("Sara");
names.add("Ali");
System.out.println(names.size());
System.out.println(names.contains("Sara"));
System.out.println(names);
}
}
Map & HashMap
Map stores key-value pairs. HashMap is the standard implementation, using hashing for fast lookups. Keys must be unique; putting a new value for an existing key overwrites the old one.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Ali", 85);
grades.put("Sara", 92);
grades.put("Ali", 90);
System.out.println(grades.get("Ali"));
System.out.println(grades.containsKey("Sara"));
System.out.println(grades);
}
}
Iterators
An Iterator steps through a collection element by element. hasNext() checks for more elements, next() returns the next one, and remove() safely deletes the current element during iteration.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
Iterator<String> it = items.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("B")) {
it.remove();
}
}
System.out.println(items);
}
}
Comparable & Comparator
Comparable defines a class's natural order with compareTo(). Comparator defines an external order with compare(). Use Comparable for one natural sort, and Comparator for multiple custom sorts without changing the class.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + "(" + age + ")";
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Ali", 25));
students.add(new Student("Sara", 22));
students.add(new Student("Zain", 28));
Collections.sort(students, Comparator.comparingInt(s -> s.age));
for (Student s : students) {
System.out.println(s);
}
}
}
String vs StringBuilder vs StringBuffer
String is immutable — every change creates a new object. StringBuilder is mutable and faster for many modifications, but not thread-safe. StringBuffer is mutable and thread-safe but slower. For loops building text, use StringBuilder.
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
System.out.println(sb.toString());
System.out.println(sb.reverse());
}
}
Autoboxing & Unboxing
Autoboxing automatically converts a primitive to its wrapper (int to Integer). Unboxing converts a wrapper back to a primitive (Integer to int). Java does this silently in assignments and method calls.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
int sum = 0;
for (Integer n : nums) {
sum += n;
}
System.out.println(sum);
}
}
Varargs (Variable Arguments)
Varargs let a method accept zero or more arguments of the same type using ... syntax. Internally, the arguments are passed as an array. A method can have only one varargs parameter, and it must be last.
public class Main {
static int sum(int... nums) {
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3));
System.out.println(sum(10, 20));
System.out.println(sum());
}
}
Static Blocks & Initializers
A static block runs once when the class is first loaded, before any object is created. It is used to initialize static fields. Instance initializer blocks run before constructors for each new object.
public class Main {
static int staticValue;
static {
staticValue = 42;
System.out.println("Static block ran");
}
public static void main(String[] args) {
System.out.println("Static value: " + staticValue);
}
}
Inner Classes
An inner class is a class defined inside another class. Non-static inner classes can access the outer class's members, including private ones. Static nested classes behave like top-level classes but are grouped logically.
class Outer {
private String message = "Hello";
class Inner {
void print() {
System.out.println(message);
}
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.print();
}
}
Intermediate Recap + Projects
You now know overloading, overriding, inheritance, super, abstract classes, interfaces, polymorphism, encapsulation, packages, exceptions, generics, collections (List, Set, Map), iterators, Comparable/Comparator, StringBuilder, autoboxing, varargs, static blocks, and inner classes. Apply them in projects combining multiple concepts.
import java.util.ArrayList;
import java.util.List;
class Task {
String name;
boolean done;
Task(String name) {
this.name = name;
}
public String toString() {
return (done ? "[x] " : "[ ] ") + name;
}
}
public class Main {
public static void main(String[] args) {
List<Task> tasks = new ArrayList<>();
tasks.add(new Task("Learn Java"));
tasks.add(new Task("Build project"));
tasks.get(0).done = true;
for (Task t : tasks) {
System.out.println(t);
}
}
}
Java Intermediate Projects
Apply OOP, collections, exceptions, and generics. Click each project to open its full guide.
About this project
Build a menu-driven to-do list that can add, view, mark complete, and remove tasks. Tasks are stored in an ArrayList and managed with methods.
What you'll practice
Step-by-step: How it works
- A
Taskclass holds the name and done flag. - An
ArrayList<Task>stores all tasks. - A
switchmenu lets the user choose an action. - Option 1 adds a task, option 2 lists tasks, option 3 marks a task done.
- Option 4 removes a task, option 5 exits the loop.
- A
whileloop keeps the menu running until exit.
import java.util.ArrayList;
import java.util.Scanner;
class Task {
String name;
boolean done;
Task(String name) {
this.name = name;
}
public String toString() {
return (done ? "[x] " : "[ ] ") + name;
}
}
public class ToDoList {
public static void main(String[] args) {
ArrayList tasks = new ArrayList<>();
Scanner sc = new Scanner(System.in);
int choice;
do {
System.out.println("\n1. Add task");
System.out.println("2. View tasks");
System.out.println("3. Mark done");
System.out.println("4. Remove task");
System.out.println("5. Exit");
System.out.print("Choose: ");
choice = sc.nextInt();
sc.nextLine();
switch (choice) {
case 1:
System.out.print("Task name: ");
tasks.add(new Task(sc.nextLine()));
break;
case 2:
for (int i = 0; i < tasks.size(); i++) {
System.out.println(i + ". " + tasks.get(i));
}
break;
case 3:
System.out.print("Task number to mark done: ");
int doneIndex = sc.nextInt();
if (doneIndex >= 0 && doneIndex < tasks.size()) {
tasks.get(doneIndex).done = true;
}
break;
case 4:
System.out.print("Task number to remove: ");
int removeIndex = sc.nextInt();
if (removeIndex >= 0 && removeIndex < tasks.size()) {
tasks.remove(removeIndex);
}
break;
}
} while (choice != 5);
sc.close();
}
}
About this project
Build a bank account with deposit, withdraw, and balance check — using encapsulation, private fields, and custom exceptions for insufficient funds.
What you'll practice
Step-by-step: How it works
Accounthas a private balance field.deposit()adds money (rejects negatives).withdraw()checks funds and throwsInsufficientFundsExceptionif short.getBalance()returns the current balance.- The main method uses try/catch to handle the custom exception.
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
class Account {
private double balance = 0;
void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: " + amount);
}
}
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Insufficient funds!");
}
balance -= amount;
System.out.println("Withdrawn: " + amount);
}
double getBalance() {
return balance;
}
}
public class BankManager {
public static void main(String[] args) {
Account acc = new Account();
acc.deposit(100);
try {
acc.withdraw(40);
acc.withdraw(100);
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("Final balance: " + acc.getBalance());
}
}
About this project
Count how many times each word appears in a sentence using a HashMap. This demonstrates the power of Map for counting and fast lookups.
What you'll practice
Step-by-step: How it works
- The sentence is split into words using
split(" "). - A
HashMap<String, Integer>stores word counts. - For each word,
getOrDefault()fetches the current count (or 0). - The count is incremented and put back.
- A for-each over
entrySet()prints each word and count.
import java.util.HashMap;
import java.util.Map;
public class WordCounter {
public static void main(String[] args) {
String sentence = "the cat and the dog";
String[] words = sentence.split(" ");
Map counts = new HashMap<>();
for (String word : words) {
int current = counts.getOrDefault(word, 0);
counts.put(word, current + 1);
}
for (Map.Entry entry : counts.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
Intermediate Capstone Project
Combine OOP, inheritance, interfaces, collections, and exceptions into one complete project.
About this project
Build a simple library system with an abstract Item class, a Book subclass, and an ArrayList catalog. It supports adding books and listing all items using polymorphism.
What you'll practice
Step-by-step: How it works
- An abstract
Itemclass definestitleand an abstractinfo(). Bookextends Item, adds an author, and overridesinfo().- An
ArrayList<Item>stores books. - Adding uses
add(). - Listing loops through and calls
info(), which dispatches to Book's version at runtime.
import java.util.ArrayList;
import java.util.List;
abstract class Item {
String title;
Item(String title) {
this.title = title;
}
abstract void info();
}
class Book extends Item {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void info() {
System.out.println("Book: " + title + " by " + author);
}
}
public class Library {
public static void main(String[] args) {
List- catalog = new ArrayList<>();
catalog.add(new Book("Effective Java", "Joshua Bloch"));
catalog.add(new Book("Clean Code", "Robert Martin"));
for (Item item : catalog) {
item.info();
}
}
}