HSHimanshu Sharma
Basic Java

Java for beginners, part 2: OOP, collections, generics, exceptions and files

Java for beginners, part 2: OOP, collections, generics, exceptions and files Advertisement

In part 1 you learned the basics: variables, control flow, methods and arrays. This part moves to what makes Java Java: classes and objects, inheritance and interfaces, the collections framework, generics, exceptions and files. Part 3 covers streams, concurrency, databases and testing. Every program was compiled with javac -Xlint:all and run on OpenJDK 21.

Classes and objects

A class is a blueprint; an object is one instance of it. A class has fields (state), constructors (to initialise), and methods (behaviour). Keep fields private and give controlled access through methods; this is encapsulation.

public class Account {
    private static int created = 0;            // shared by all accounts
    private final String owner;                // set once
    private double balance;

    public Account(String owner, double opening) {
        if (opening < 0) throw new IllegalArgumentException("opening balance cannot be negative");
        this.owner = owner;
        this.balance = opening;
        created++;
    }

    public Account(String owner) { this(owner, 0); }      // constructor chaining

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("deposit must be positive");
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount > balance) return false;
        balance -= amount;
        return true;
    }

    public String getOwner() { return owner; }
    public double getBalance() { return balance; }
    public static int getCreated() { return created; }

    @Override
    public String toString() { return owner + ": " + balance; }

    public static void main(String[] args) {
        Account a = new Account("Asha", 1000);
        Account b = new Account("Ravi");
        a.deposit(250.5);
        System.out.println(a + " | " + b + " | withdraw 5000 -> " + a.withdraw(5000) + " | accounts: " + Account.getCreated());
        try {
            new Account("X", -5);
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}
Asha: 1250.5 | Ravi: 0.0 | withdraw 5000 -> false | accounts: 2
rejected: opening balance cannot be negative

Points to notice:

  • this refers to the current object; this(owner, 0) calls another constructor of the same class.
  • static members belong to the class, not to an object (created counts all accounts). Static methods cannot use instance fields.
  • final fields can be set only once, in the constructor.
  • @Override on toString asks the compiler to check that you really are overriding a method; every class inherits toString, equals and hashCode from Object.
  • If you write no constructor, Java adds an empty default one; if you write any, it does not.
  • Validation in the constructor means an invalid Account can never exist.

Inheritance and polymorphism

A class can extend one other class and inherit its non-private members. A subclass can override methods, and calls through a parent-typed variable run the object's version (polymorphism). super refers to the parent.

public class Animals {
    static class Animal {
        protected final String name;
        Animal(String name) { this.name = name; }
        String sound() { return "..."; }
        public String toString() { return name + " says " + sound(); }
    }
    static class Dog extends Animal {
        Dog(String name) { super(name); }
        @Override String sound() { return "Woof"; }
        void fetch() { System.out.println(name + " fetches"); }
    }
    static class Puppy extends Dog {
        Puppy(String name) { super(name); }
        @Override String sound() { return super.sound() + " (squeaky)"; }
    }
    static class Cat extends Animal {
        Cat(String name) { super(name); }
        @Override String sound() { return "Meow"; }
    }

    public static void main(String[] args) {
        Animal[] zoo = {new Dog("Rex"), new Cat("Tom"), new Puppy("Bit"), new Animal("Thing")};
        for (Animal a : zoo) System.out.println(a);
        Animal first = zoo[0];
        if (first instanceof Dog d) d.fetch();               // pattern matching for instanceof
        System.out.println(zoo[2] instanceof Dog);           // a Puppy is a Dog
    }
}
Rex says Woof
Tom says Meow
Bit says Woof (squeaky)
Thing says ...
Rex fetches
true

Java has single inheritance of classes (one parent), which avoids the diamond problem; multiple inheritance of behaviour comes through interfaces. The ideas match the Python inheritance post, with stricter typing. Access modifiers: private (class only), package-private (default; same package), protected (package and subclasses), public (everywhere).

Abstract classes and interfaces

An abstract class cannot be instantiated and may declare abstract methods that subclasses must implement. An interface is a pure contract of methods; a class can implement many interfaces. Since Java 8, interfaces can have default methods with a body.

import java.util.List;

public class Shapes {
    interface Shape {
        double area();
        default String describe() { return getClass().getSimpleName() + " with area " + String.format("%.2f", area()); }
    }
    static abstract class Base implements Shape {
        public String toString() { return describe(); }
    }
    static class Rect extends Base {
        final double w, h;
        Rect(double w, double h) { this.w = w; this.h = h; }
        public double area() { return w * h; }
    }
    static class Circle extends Base {
        final double r;
        Circle(double r) { this.r = r; }
        public double area() { return Math.PI * r * r; }
    }

    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Rect(3, 4), new Circle(1));
        double total = 0;
        for (Shape s : shapes) {
            System.out.println(s.describe());
            total += s.area();
        }
        System.out.printf("total %.3f%n", total);
        Shape anonymous = () -> 2.0;                                 // an interface with one abstract method can be a lambda
        System.out.println(anonymous.area());
    }
}
Rect with area 12.00
Circle with area 3.14
total 15.142
2.0

Choose an interface for "can do" capabilities (Comparable, Runnable, Shape) and an abstract class when subclasses share real state and code. Program to the interface (List<Shape>), not the implementation.

Enums and records

An enum is a fixed set of named constants, and can have fields and methods. A record (Java 16+) is a compact immutable data class: the compiler generates the constructor, accessors, equals, hashCode and toString.

public class EnumsAndRecords {
    enum Level {
        LOW(1), MEDIUM(5), HIGH(10);
        private final int weight;
        Level(int weight) { this.weight = weight; }
        int weight() { return weight; }
    }

    record Point(int x, int y) {
        Point {                                          // compact constructor: validation
            if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate");
        }
        double distanceTo(Point other) { return Math.hypot(x - other.x, y - other.y); }
    }

    public static void main(String[] args) {
        Level l = Level.valueOf("HIGH");
        System.out.println(l + " " + l.weight() + " " + l.ordinal() + " " + java.util.Arrays.toString(Level.values()));
        Point p = new Point(3, 4);
        System.out.println(p + " " + p.x() + " " + p.equals(new Point(3, 4)) + " " + p.distanceTo(new Point(0, 0)));
        switch (l) {
            case LOW -> System.out.println("relax");
            case HIGH -> System.out.println("act now");
            default -> System.out.println("watch");
        }
    }
}
HIGH 10 2 [LOW, MEDIUM, HIGH]
Point[x=3, y=4] 3 true 5.0
act now

The collections framework

Arrays have a fixed size. The java.util collections grow, shrink and offer ready-made operations. The three families:

Interface Meaning Common implementations
List ordered, allows duplicates, index access ArrayList (default choice), LinkedList
Set no duplicates HashSet (fast, unordered), LinkedHashSet (insertion order), TreeSet (sorted)
Map key-to-value pairs HashMap (fast), LinkedHashMap, TreeMap (sorted by key)
Queue / Deque first-in-first-out / both ends ArrayDeque, PriorityQueue
import java.util.*;

public class Collections1 {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Ravi", "Asha", "Meena"));
        names.add("Kabir");
        names.remove("Ravi");
        Collections.sort(names);
        System.out.println(names + " " + names.size() + " " + names.contains("Asha") + " " + names.indexOf("Meena") + " " + names.get(0));

        Set<Integer> unique = new TreeSet<>(Arrays.asList(5, 3, 9, 3, 5, 1));
        System.out.println(unique + " " + new HashSet<>(List.of(1, 2)).containsAll(List.of(1)));

        Map<String, Integer> marks = new HashMap<>();
        marks.put("Asha", 82);
        marks.put("Ravi", 67);
        marks.merge("Asha", 5, Integer::sum);                     // 87
        marks.putIfAbsent("Meena", 91);
        System.out.println(new TreeMap<>(marks) + " " + marks.getOrDefault("Zed", -1));
        for (Map.Entry<String, Integer> e : new TreeMap<>(marks).entrySet())
            System.out.print(e.getKey() + "=" + e.getValue() + ";");
        System.out.println();

        Map<Character, Integer> counts = new TreeMap<>();
        for (char c : "banana".toCharArray()) counts.merge(c, 1, Integer::sum);
        System.out.println(counts);

        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(1); stack.push(2);
        Queue<Integer> queue = new ArrayDeque<>();
        queue.offer(1); queue.offer(2);
        System.out.println(stack.pop() + " " + queue.poll());

        PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(5, 1, 3));
        System.out.println(pq.poll() + " " + pq.poll());

        List<Integer> fixed = List.of(1, 2, 3);
        try { fixed.add(4); } catch (UnsupportedOperationException e) { System.out.println("List.of is immutable"); }
    }
}
[Asha, Kabir, Meena] 3 true 2 Asha
[1, 3, 5, 9] true
{Asha=87, Meena=91, Ravi=67} -1
Asha=87;Meena=91;Ravi=67;
{a=3, b=1, n=2}
2 1
1 3
List.of is immutable

Key rules: a HashMap or HashSet relies on equals and hashCode of the elements (records and strings already have them; for your own classes, override both together). List.of, Set.of and Map.of create immutable collections. Never modify a list while looping over it with for-each: you get a ConcurrentModificationException; use an Iterator's remove or removeIf.

Sorting with Comparable and Comparator

import java.util.*;

public class Sorting {
    record Student(String name, int marks, String city) {}

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>(List.of(
            new Student("Asha", 82, "Pune"), new Student("Ravi", 67, "Delhi"),
            new Student("Meena", 91, "Pune"), new Student("Kabir", 82, "Goa")));

        list.sort(Comparator.comparingInt(Student::marks).reversed().thenComparing(Student::name));
        for (Student s : list) System.out.print(s.name() + ":" + s.marks() + " ");
        System.out.println();

        list.sort(Comparator.comparing(Student::city).thenComparing(Student::name, Comparator.reverseOrder()));
        for (Student s : list) System.out.print(s.city() + "/" + s.name() + " ");
        System.out.println();
    }
}
Meena:91 Asha:82 Kabir:82 Ravi:67 
Delhi/Ravi Goa/Kabir Pune/Meena Pune/Asha 

A class gets a natural order by implementing Comparable<T>; a Comparator supplies any other order. Chaining comparing(...).thenComparing(...) replaces the old hand-written comparison methods.

Generics

Generics let a class or method work with any type while keeping type safety. Without them, a list of Object needs casts and fails at run time; with them, the compiler catches the mistake.

import java.util.*;

public class Generics {
    static class Box<T> {
        private T value;
        Box(T value) { this.value = value; }
        T get() { return value; }
        void set(T value) { this.value = value; }
    }

    static <T extends Comparable<T>> T maxOf(List<T> items) {      // bounded type parameter
        T best = items.get(0);
        for (T item : items) if (item.compareTo(best) > 0) best = item;
        return best;
    }

    static double sum(List<? extends Number> numbers) {            // wildcard: any list of numbers
        double total = 0;
        for (Number n : numbers) total += n.doubleValue();
        return total;
    }

    public static void main(String[] args) {
        Box<String> box = new Box<>("hello");
        String s = box.get();                                       // no cast needed
        System.out.println(s.toUpperCase() + " " + new Box<>(42).get());
        System.out.println(maxOf(List.of(3, 9, 4)) + " " + maxOf(List.of("pear", "apple", "zebra")));
        System.out.println(sum(List.of(1, 2, 3)) + " " + sum(List.of(1.5, 2.5)));
    }
}
HELLO 42
9 zebra
6.0 4.0

<T> declares a type parameter; T extends Comparable<T> limits it to types that can be compared; ? extends Number accepts a list of any number type. Generics are checked at compile time and erased at run time, so you cannot write new T() or instanceof List<String>; that is a common interview question.

Exceptions

An exception is an object that signals an error and interrupts normal flow until something handles it. Java has two kinds:

  • Checked exceptions (subclasses of Exception but not RuntimeException, like IOException) must be caught or declared with throws; the compiler enforces it. They represent problems that callers can reasonably recover from.
  • Unchecked exceptions (RuntimeException and its children: NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException, ArithmeticException) signal bugs and need not be declared. Errors (such as OutOfMemoryError) should not be caught.
import java.io.IOException;

public class Exceptions {
    static class InsufficientFundsException extends Exception {          // custom checked exception
        private static final long serialVersionUID = 1L;
        final double shortBy;
        InsufficientFundsException(double shortBy) {
            super("short by " + shortBy);
            this.shortBy = shortBy;
        }
    }

    static void withdraw(double balance, double amount) throws InsufficientFundsException {
        if (amount > balance) throw new InsufficientFundsException(amount - balance);
        System.out.println("ok, new balance " + (balance - amount));
    }

    static class Connection implements AutoCloseable {
        final String name;
        Connection(String name) { this.name = name; System.out.println("open " + name); }
        void send() throws IOException { throw new IOException("network down"); }
        @Override public void close() { System.out.println("close " + name); }
    }

    public static void main(String[] args) {
        try {
            withdraw(100, 30);
            withdraw(100, 250);
        } catch (InsufficientFundsException e) {
            System.out.println("caught: " + e.getMessage());
        } finally {
            System.out.println("finally runs always");
        }

        try (Connection c = new Connection("db")) {                      // try-with-resources
            c.send();
        } catch (IOException e) {
            System.out.println("io error: " + e.getMessage());
        }

        try {
            Object o = null;
            o.toString();
        } catch (NullPointerException | ArithmeticException e) {         // multi-catch
            System.out.println(e.getClass().getSimpleName());
        }
        try {
            System.out.println(10 / (args.length));
        } catch (ArithmeticException e) {
            System.out.println("ArithmeticException: " + e.getMessage());
        }
    }
}
ok, new balance 70.0
caught: short by 150.0
finally runs always
open db
close db
io error: network down
NullPointerException
ArithmeticException: / by zero

try-with-resources closes anything that implements AutoCloseable automatically, even when an exception is thrown: notice close db printed before the catch ran. Use it for files, database connections and streams, instead of writing finally blocks by hand. Good practice: catch specific exceptions, never swallow them silently (catch (Exception e) {}), keep the original cause when wrapping (throw new RuntimeException("context", e)), and see the same ideas in Python exception handling.

Files

The modern API is java.nio.file (Files and Path). It works with whole files in one call, and with streams for large ones.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;

public class Files1 {
    public static void main(String[] args) throws IOException {
        Path dir = Files.createTempDirectory("demo");
        Path file = dir.resolve("notes.txt");

        Files.write(file, List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
        Files.writeString(file, "delta\n", StandardOpenOption.APPEND);

        List<String> lines = Files.readAllLines(file);
        System.out.println(lines + " " + Files.size(file) + " bytes, exists=" + Files.exists(file));

        try (var stream = Files.lines(file)) {                         // streams a big file lazily
            System.out.println(stream.filter(l -> l.contains("a")).count() + " lines contain 'a'");
        }
        try {
            Files.readString(dir.resolve("missing.txt"));
        } catch (NoSuchFileException e) {
            System.out.println("NoSuchFileException: missing.txt");
        }
        Files.delete(file);
        Files.delete(dir);
        System.out.println("cleaned up: " + !Files.exists(dir));
    }
}
[alpha, beta, gamma, delta] 23 bytes, exists=true
4 lines contain 'a'
NoSuchFileException: missing.txt
cleaned up: true

Always state the character set (UTF_8), close streams (try-with-resources), and check for errors: file operations throw the checked IOException. Compare with file handling in Python.

Common mistakes

  • Making fields public instead of private with methods.
  • Overriding equals without hashCode (hash-based collections then misbehave).
  • Calling an overridable method from a constructor.
  • Catching Exception too broadly or leaving the catch block empty.
  • Forgetting to close resources (use try-with-resources).
  • Using raw types (List instead of List<String>), which disables type checks.
  • Modifying a collection during a for-each loop.
  • Confusing == with equals for objects.

Practice

Model a library: Book (a record), Member, a Library class holding a Map<String, Book> and a List of loans, an Exception for "book not available", and a method that saves the loan list to a file and reads it back. Sort books by title and by year with comparators. Then try an interface Notifier with email and SMS implementations.

Next

Continue with part 3: streams, lambdas, concurrency, databases with JDBC and testing, or go back to the introduction.

Advertisement